@ix-xs/node-comfort 1.0.4 → 1.0.5
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/index.js +12 -14
- package/package.json +17 -17
- package/src/checker.js +76 -0
- package/src/cli.js +86 -0
- package/{core/FS.js → src/fs.js} +121 -229
- package/src/localStorage.js +242 -0
- package/{core/Logger.js → src/logger.js} +23 -97
- package/src/utils.js +225 -0
- package/tsconfig.json +1 -1
- package/types/index.d.ts +71 -53
- package/types/src/checker.d.ts +49 -0
- package/types/src/checker.d.ts.map +1 -0
- package/types/src/cli.d.ts +26 -0
- package/types/src/cli.d.ts.map +1 -0
- package/types/src/fs.d.ts +210 -0
- package/types/src/fs.d.ts.map +1 -0
- package/types/src/localStorage.d.ts +97 -0
- package/types/src/localStorage.d.ts.map +1 -0
- package/types/src/logger.d.ts +132 -0
- package/types/src/logger.d.ts.map +1 -0
- package/types/src/utils.d.ts +92 -0
- package/types/src/utils.d.ts.map +1 -0
- package/core/CLI.js +0 -95
- package/core/Checker.js +0 -127
- package/core/Stepper.js +0 -115
- package/core/Storage.js +0 -348
- package/core/Utils.js +0 -206
- package/types/core/CLI.d.ts +0 -25
- package/types/core/CLI.d.ts.map +0 -1
- package/types/core/Checker.d.ts +0 -73
- package/types/core/Checker.d.ts.map +0 -1
- package/types/core/FS.d.ts +0 -220
- package/types/core/FS.d.ts.map +0 -1
- package/types/core/Logger.d.ts +0 -253
- package/types/core/Logger.d.ts.map +0 -1
- package/types/core/Stepper.d.ts +0 -49
- package/types/core/Stepper.d.ts.map +0 -1
- package/types/core/Storage.d.ts +0 -111
- package/types/core/Storage.d.ts.map +0 -1
- package/types/core/Utils.d.ts +0 -55
- package/types/core/Utils.d.ts.map +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const fs = require("./fs");
|
|
2
|
+
const checker = require("./checker");
|
|
3
|
+
|
|
4
|
+
const originalEmitWarning = process.emitWarning;
|
|
5
|
+
process.emitWarning = (warning, ...args) => {
|
|
6
|
+
if ((warning.name === "ExperimentalWarning" && warning.message.includes("SQLite")) ||
|
|
7
|
+
(typeof warning === "string" && warning.startsWith("SQLite is an experimental feature"))) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
return originalEmitWarning.call(process, warning, ...args);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
14
|
+
|
|
15
|
+
module.exports = class localStorage {
|
|
16
|
+
#$;
|
|
17
|
+
#models = new Map();
|
|
18
|
+
|
|
19
|
+
constructor(path = fs.createPath("localStorage/localStorage.sqlite")) {
|
|
20
|
+
if (path.includes("/")) {
|
|
21
|
+
const split = path.split("/");
|
|
22
|
+
const file = split[split.length - 1];
|
|
23
|
+
const folder = fs.createPath(path.replace(file, ""));
|
|
24
|
+
fs.createFolder(folder);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.#$ = new DatabaseSync(path);
|
|
28
|
+
this.#exec("PRAGMA journal_mode = WAL");
|
|
29
|
+
this.#exec("PRAGMA foreign_keys = ON");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#exec(sql, options = []) {
|
|
33
|
+
try {
|
|
34
|
+
const stmt = this.#$.prepare(sql);
|
|
35
|
+
const result = stmt.run(...checker.isArray(options) ? options : [options]);
|
|
36
|
+
return {
|
|
37
|
+
changes: result.changes,
|
|
38
|
+
lastInsertRowid: result.lastInsertRowid,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
#buildCreateSQL(tableName, options) {
|
|
45
|
+
const columns = Object.entries(options).map(([col, def]) => {
|
|
46
|
+
let colDef = `${col} ${def.type}`;
|
|
47
|
+
|
|
48
|
+
if (def.primaryKey) {
|
|
49
|
+
colDef += " PRIMARY KEY";
|
|
50
|
+
if (def.autoincrement) colDef += " AUTOINCREMENT";
|
|
51
|
+
}
|
|
52
|
+
if (def.notNull) colDef += " NOT NULL";
|
|
53
|
+
if (def.unique) colDef += " UNIQUE";
|
|
54
|
+
if (def.defaultValue !== undefined) colDef += ` DEFAULT ${JSON.stringify(def.defaultValue)}`;
|
|
55
|
+
if (def.values?.length) {
|
|
56
|
+
colDef += ` CHECK(${col} IN (${def.values.map(v => JSON.stringify(v)).join(", ")}))`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return colDef;
|
|
60
|
+
}).join(", ");
|
|
61
|
+
|
|
62
|
+
return `CREATE TABLE IF NOT EXISTS ${tableName} (${columns})`;
|
|
63
|
+
}
|
|
64
|
+
#defineModel(tableName, options) {
|
|
65
|
+
if (this.#models.has(tableName)) return;
|
|
66
|
+
|
|
67
|
+
const createSQL = this.#buildCreateSQL(tableName, options);
|
|
68
|
+
this.#exec(createSQL);
|
|
69
|
+
this.#models.set(tableName, { name: tableName, options });
|
|
70
|
+
}
|
|
71
|
+
#buildWhere(where) {
|
|
72
|
+
if (Object.keys(where).length === 0) {
|
|
73
|
+
return { where: "", params: [] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const conditions = [];
|
|
77
|
+
const params = [];
|
|
78
|
+
|
|
79
|
+
for (const [key, value] of Object.entries(where)) {
|
|
80
|
+
conditions.push(`${key} = ?`);
|
|
81
|
+
params.push(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
where: `WHERE ${conditions.join(" AND ")}`,
|
|
86
|
+
params,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
#getOrderBy(tableName) {
|
|
90
|
+
const model = this.#models.get(tableName);
|
|
91
|
+
if (!model) return "id DESC";
|
|
92
|
+
const firstCol = Object.keys(model.options)[0];
|
|
93
|
+
return `${firstCol} DESC`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {{name: string, options: Record<string, {
|
|
98
|
+
* type: "TEXT"|"INTEGER"|"REAL"|"BLOB"|"NUMERIC"|"DATE"|"DATETIME"|"TIMESTAMP"|"TIME"|"VARCHAR"|"CHAR"|"CLOB",
|
|
99
|
+
* primaryKey?: boolean,
|
|
100
|
+
* unique?: boolean,
|
|
101
|
+
* notNull: boolean,
|
|
102
|
+
* defaultValue?: any,
|
|
103
|
+
* values?: Array<string>
|
|
104
|
+
* autoincrement?: boolean,
|
|
105
|
+
* }>}} table
|
|
106
|
+
*/
|
|
107
|
+
async createTable(table) {
|
|
108
|
+
try {
|
|
109
|
+
this.#defineModel(table.name, table.options);
|
|
110
|
+
return { ok: true };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return { error: error.message };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} tableName
|
|
117
|
+
*/
|
|
118
|
+
async deleteTable(tableName) {
|
|
119
|
+
try {
|
|
120
|
+
if (!this.#models.has(tableName)) return { error: `Table ${tableName} doesn't exist` };
|
|
121
|
+
|
|
122
|
+
this.#exec(`DROP TABLE IF EXISTS ${tableName}`);
|
|
123
|
+
this.#models.delete(tableName);
|
|
124
|
+
return { ok: true };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return { error: error.message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} tableName
|
|
131
|
+
*/
|
|
132
|
+
async clearTable(tableName) {
|
|
133
|
+
try {
|
|
134
|
+
if (!this.#models.has(tableName)) return { error: `Table ${tableName} doesn't exist` };
|
|
135
|
+
|
|
136
|
+
this.#exec(`DELETE FROM ${tableName}`);
|
|
137
|
+
return { ok: true };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { error: error.message };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} tableName
|
|
144
|
+
* @param {object} [where]
|
|
145
|
+
*/
|
|
146
|
+
async get(tableName, where) {
|
|
147
|
+
try {
|
|
148
|
+
if (!this.#models.has(tableName)) return undefined;
|
|
149
|
+
|
|
150
|
+
const whereClause = this.#buildWhere(where);
|
|
151
|
+
const sql = `SELECT * FROM ${tableName} ${whereClause.where} LIMIT 1`;
|
|
152
|
+
const stmt = this.#$.prepare(sql);
|
|
153
|
+
const record = stmt.get(whereClause.params);
|
|
154
|
+
return record || undefined;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { error: error.message };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} tableName
|
|
161
|
+
* @param {object} [where]
|
|
162
|
+
* @param {number} [limit]
|
|
163
|
+
* @param {number} [offset]
|
|
164
|
+
*/
|
|
165
|
+
async getAll(tableName, where = {}, limit = 50, offset = 0) {
|
|
166
|
+
try {
|
|
167
|
+
if (!this.#models.has(tableName)) return [];
|
|
168
|
+
|
|
169
|
+
const whereClause = this.#buildWhere(where);
|
|
170
|
+
const sql = `SELECT * FROM ${tableName} ${whereClause.where} ORDER BY ${this.#getOrderBy(tableName)} LIMIT ? OFFSET ?`;
|
|
171
|
+
const stmt = this.#$.prepare(sql);
|
|
172
|
+
const records = stmt.all(...whereClause.params, limit, offset);
|
|
173
|
+
return records;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return { error: error.message };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* @param {string} tableName
|
|
180
|
+
* @param {*} data
|
|
181
|
+
* @param {object} [where]
|
|
182
|
+
*/
|
|
183
|
+
async set(tableName, data, where = null) {
|
|
184
|
+
try {
|
|
185
|
+
if (!this.#models.has(tableName)) return { error: `Table ${tableName} doesn't exist` };
|
|
186
|
+
|
|
187
|
+
const lookup = where ?? (data.id ? { id: data.id } : null);
|
|
188
|
+
|
|
189
|
+
if (lookup) {
|
|
190
|
+
const columns = Object.keys(data).join(", ");
|
|
191
|
+
const placeholders = Object.keys(data).map(() => "?").join(", ");
|
|
192
|
+
const sql = `INSERT OR REPLACE INTO ${tableName} (${columns}) VALUES (${placeholders})`;
|
|
193
|
+
const stmt = this.#$.prepare(sql);
|
|
194
|
+
stmt.run(...Object.values(data));
|
|
195
|
+
return { ok: true };
|
|
196
|
+
} else {
|
|
197
|
+
const columns = Object.keys(data).join(", ");
|
|
198
|
+
const placeholders = Object.keys(data).map(() => "?").join(", ");
|
|
199
|
+
const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`;
|
|
200
|
+
const stmt = this.#$.prepare(sql);
|
|
201
|
+
stmt.run(...Object.values(data));
|
|
202
|
+
return { ok: true };
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return { error: error.message };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* @param {string} tableName
|
|
210
|
+
* @param {object} where
|
|
211
|
+
*/
|
|
212
|
+
async delete(tableName, where) {
|
|
213
|
+
try {
|
|
214
|
+
if (!this.#models.has(tableName)) return { error: `Table ${tableName} doesn't exist` };
|
|
215
|
+
|
|
216
|
+
const whereClause = this.#buildWhere(where);
|
|
217
|
+
const sql = `DELETE FROM ${tableName} ${whereClause.where}`;
|
|
218
|
+
const stmt = this.#$.prepare(sql);
|
|
219
|
+
const result = stmt.run(...whereClause.params);
|
|
220
|
+
return { ok: true, changes: result.changes };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { error: error.message };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} tableName
|
|
227
|
+
* @param {object} where
|
|
228
|
+
*/
|
|
229
|
+
async count(tableName, where = {}) {
|
|
230
|
+
try {
|
|
231
|
+
if (!this.#models.has(tableName)) return { error: `Table ${tableName} doesn't exist` };
|
|
232
|
+
|
|
233
|
+
const whereClause = this.#buildWhere(where);
|
|
234
|
+
const sql = `SELECT COUNT(*) as total FROM ${tableName} ${whereClause.where}`;
|
|
235
|
+
const stmt = this.#$.prepare(sql);
|
|
236
|
+
const result = stmt.get(...whereClause.params);
|
|
237
|
+
return result.total;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { error: error.message };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
@@ -56,11 +56,9 @@ const _options = {
|
|
|
56
56
|
const _groups = [];
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
* @returns {number|null} ANSI code or null.
|
|
63
|
-
*/
|
|
59
|
+
* @param {string} name
|
|
60
|
+
* @returns {number|null}
|
|
61
|
+
*/
|
|
64
62
|
const _getCode = (name) => {
|
|
65
63
|
if (_style[name] !== undefined) return _style[name];
|
|
66
64
|
if (_color.fg[name] !== undefined) return _color.fg[name];
|
|
@@ -70,12 +68,9 @@ const _getCode = (name) => {
|
|
|
70
68
|
}
|
|
71
69
|
return null;
|
|
72
70
|
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
* @private
|
|
77
|
-
* @param {string} input - Template string.
|
|
78
|
-
* @returns {{codes: number[], text: string}} Parsed result.
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} input
|
|
73
|
+
* @returns {{codes: number[], text: string}}
|
|
79
74
|
*/
|
|
80
75
|
const _parse = (input) => {
|
|
81
76
|
const tokens = input.trim().split(/\s+/);
|
|
@@ -108,10 +103,7 @@ const _parse = (input) => {
|
|
|
108
103
|
.trimStart();
|
|
109
104
|
return { codes, text };
|
|
110
105
|
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Builds ANSI escape sequence from codes.
|
|
114
|
-
* @private
|
|
106
|
+
/**
|
|
115
107
|
* @param {number[]} codes - ANSI codes.
|
|
116
108
|
* @returns {string} Escape sequence.
|
|
117
109
|
*/
|
|
@@ -119,7 +111,6 @@ const _build = (codes) => {
|
|
|
119
111
|
if (codes.length === 0) return "";
|
|
120
112
|
return `${_x}[${codes.join(";")}m`;
|
|
121
113
|
};
|
|
122
|
-
|
|
123
114
|
const _apply = (str) => {
|
|
124
115
|
const esc_start = _options.delimiters.start.replace(
|
|
125
116
|
/[.*+?^${}()|[\]\\]/g,
|
|
@@ -148,9 +139,7 @@ const _apply = (str) => {
|
|
|
148
139
|
},
|
|
149
140
|
);
|
|
150
141
|
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* @private
|
|
142
|
+
/**
|
|
154
143
|
* @returns {string}
|
|
155
144
|
*/
|
|
156
145
|
const _timestamp = () => {
|
|
@@ -159,54 +148,17 @@ const _timestamp = () => {
|
|
|
159
148
|
`<%gray [${new Date().toLocaleTimeString(undefined, { hour12: false })}]%>`,
|
|
160
149
|
);
|
|
161
150
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* Generates indentation based on group depth.
|
|
165
|
-
* @private
|
|
166
|
-
* @returns {string} Indentation spaces.
|
|
151
|
+
/**
|
|
152
|
+
* @returns {string}
|
|
167
153
|
*/
|
|
168
154
|
const _indent = () => {
|
|
169
155
|
return " ".repeat(0 + _groups.length);
|
|
170
156
|
};
|
|
171
157
|
|
|
172
|
-
/**
|
|
173
|
-
* @module logger
|
|
174
|
-
* @description
|
|
175
|
-
* Beautiful ANSI-colored console logger with grouping, timestamps, and template syntax.
|
|
176
|
-
*
|
|
177
|
-
* Supports 16+ colors, styles (bold, italic, underline), background colors,
|
|
178
|
-
* nested groups with indentation, and customizable delimiters.
|
|
179
|
-
*
|
|
180
|
-
* ## Syntax
|
|
181
|
-
* ```
|
|
182
|
-
* <%red bold This is red and bold%>
|
|
183
|
-
* <%bgBlue whiteBright ERROR%>
|
|
184
|
-
* <%yellowBright [WARN]%>
|
|
185
|
-
* ```
|
|
186
|
-
*
|
|
187
|
-
* Available styles: `normal`, `bold`, `italic`, `underline`, `overline`
|
|
188
|
-
* Colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`
|
|
189
|
-
* Bright colors: `redBright`, `greenBright`, `yellowBright`, `blueBright`, `magentaBright`, `cyanBright`, `whiteBright`
|
|
190
|
-
* Backgrounds: `bgBlack`, `bgRed`, `bgGreen`, etc.
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* const nodeComfort = require("@ix-xs/node-comfort");
|
|
194
|
-
*
|
|
195
|
-
* nodeComfort.log("<%greenBright ✓ Success%> Operation completed!");
|
|
196
|
-
* nodeComfort.group("<%blue Processing files%>").log("File 1").groupEnd();
|
|
197
|
-
* const log = nodeComfort.logify({ users: 42 }); // → ANSI string
|
|
198
|
-
*/
|
|
199
158
|
module.exports = {
|
|
200
159
|
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
* @param {any} content - Content to log (string, object, or primitive).
|
|
205
|
-
* @example
|
|
206
|
-
* nodeComfort.log("<%greenBright ✓ Done%>");
|
|
207
|
-
* nodeComfort.log({ users: 42, active: true });
|
|
208
|
-
* nodeComfort.group("Files").log("Processing...").groupEnd();
|
|
209
|
-
*/
|
|
160
|
+
* @param {*} content
|
|
161
|
+
*/
|
|
210
162
|
log(content) {
|
|
211
163
|
let str;
|
|
212
164
|
if (typeof content === "string") {
|
|
@@ -239,16 +191,10 @@ module.exports = {
|
|
|
239
191
|
|
|
240
192
|
return this;
|
|
241
193
|
},
|
|
242
|
-
|
|
243
194
|
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
* @returns {string} Formatted string.
|
|
248
|
-
* @example
|
|
249
|
-
* const msg = nodeComfort.logify("<%redBright ERROR%> Invalid input");
|
|
250
|
-
* console.log(msg);
|
|
251
|
-
*/
|
|
195
|
+
* @param {any} content - Content to format.
|
|
196
|
+
* @returns {string} Formatted string.
|
|
197
|
+
*/
|
|
252
198
|
logify(content) {
|
|
253
199
|
let str;
|
|
254
200
|
if (typeof content === "string") {
|
|
@@ -262,46 +208,26 @@ module.exports = {
|
|
|
262
208
|
const prefix = `${_timestamp()}${_indent()}`;
|
|
263
209
|
return _apply(prefix ? `${prefix} ${str}` : str);
|
|
264
210
|
},
|
|
265
|
-
|
|
266
211
|
/**
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
* @param {string} label - Group label (supports colors).
|
|
270
|
-
* @example
|
|
271
|
-
* nodeComfort.group("<%yellow Processing%>").log("Step 1").groupEnd();
|
|
272
|
-
*/
|
|
212
|
+
* @param {string} label
|
|
213
|
+
*/
|
|
273
214
|
group(label) {
|
|
274
215
|
_groups.push(label);
|
|
275
216
|
const prefix = `${_timestamp()}${_indent()}`;
|
|
276
217
|
console.log(prefix ? `${prefix}▼ ${label}` : `▼ ${label}`);
|
|
277
218
|
return this;
|
|
278
219
|
},
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Ends current group and reduces indentation.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* nodeComfort.group("Work").groupEnd();
|
|
285
|
-
*/
|
|
286
220
|
groupEnd() {
|
|
287
221
|
if (_groups.length > 0) _groups.pop();
|
|
288
222
|
return this;
|
|
289
223
|
},
|
|
290
|
-
|
|
291
224
|
/**
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
* @param {boolean} [options.timestamp=true] - Show timestamps.
|
|
299
|
-
* @example
|
|
300
|
-
* nodeComfort.setLogger({
|
|
301
|
-
* delimiters: { start: "{{", end: "}}" },
|
|
302
|
-
* timestamp: false
|
|
303
|
-
* }).log("{{ bgGreen new delimiters }}");
|
|
304
|
-
*/
|
|
225
|
+
* @param {object} [options]
|
|
226
|
+
* @param {object} [options.delimiters]
|
|
227
|
+
* @param {string} [options.delimiters.start="<%="]
|
|
228
|
+
* @param {string} [options.delimiters.end="%>"]
|
|
229
|
+
* @param {boolean} [options.timestamp=true]
|
|
230
|
+
*/
|
|
305
231
|
setLogger(options = _options) {
|
|
306
232
|
options = {
|
|
307
233
|
delimiters: {
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const logger = require("./logger");
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
/**
|
|
5
|
+
* @param {number} ms
|
|
6
|
+
*/
|
|
7
|
+
async wait(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
},
|
|
10
|
+
/**
|
|
11
|
+
* Polls a predicate at a given interval and emits events when it matches or times out.
|
|
12
|
+
*
|
|
13
|
+
* The predicate is evaluated every `options.interval` milliseconds (default: 50 ms).
|
|
14
|
+
* Be careful with the default interval: 50 ms is very frequent polling and can be
|
|
15
|
+
* unnecessarily CPU‑intensive if your predicate or trigger handlers are heavy.
|
|
16
|
+
* When `options.max` is `null`, the predicate can match indefinitely (infinite triggers),
|
|
17
|
+
* so you should choose an interval that makes sense for your use‑case.
|
|
18
|
+
*
|
|
19
|
+
* @param {boolean|Promise<boolean>|(() => boolean|Promise<boolean>)} predicate
|
|
20
|
+
* Condition to evaluate at each tick, or a function returning it.
|
|
21
|
+
* @param {*} payload
|
|
22
|
+
* Value passed to listeners when events are emitted (e.g. an object or message).
|
|
23
|
+
* @param {object} [options]
|
|
24
|
+
* @param {number} [options.interval=50]
|
|
25
|
+
* Polling interval in milliseconds. Increase this value if your predicate is
|
|
26
|
+
* expensive or you expect many triggers without a `max` limit.
|
|
27
|
+
* @param {number|null} [options.timeout=null]
|
|
28
|
+
* Maximum time in milliseconds before emitting a "timeout" event and stopping.
|
|
29
|
+
* If `null`, no timeout is applied.
|
|
30
|
+
* @param {number|null} [options.max=null]
|
|
31
|
+
* Maximum number of successful predicate evaluations ("trigger" events) before
|
|
32
|
+
* automatically stopping. If `null`, triggers are infinite until `stop()` is called.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Basic usage: infinite triggers every 50 ms (default interval) – use with caution.
|
|
36
|
+
* const task = when(() => true, { message: "Ok" });
|
|
37
|
+
*
|
|
38
|
+
* task.on("trigger", (payload) => {
|
|
39
|
+
* console.log(payload.message);
|
|
40
|
+
* // task.stop(); // stop when you’re done
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* task.start();
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Safer usage: limit triggers and/or lower polling frequency.
|
|
47
|
+
* when(() => true, { message: "Ok" }, { max: 1, interval: 500 }) // one trigger max, every 500 ms
|
|
48
|
+
* .on("trigger", (payload) => {
|
|
49
|
+
* console.log(payload.message);
|
|
50
|
+
* })
|
|
51
|
+
* .start();
|
|
52
|
+
*/
|
|
53
|
+
when(predicate, payload = {}, options) {
|
|
54
|
+
const interval = options?.interval ?? 50;
|
|
55
|
+
const timeout = options?.timeout ?? null;
|
|
56
|
+
const max = options?.max ?? null;
|
|
57
|
+
|
|
58
|
+
const listeners = {
|
|
59
|
+
trigger: new Set(),
|
|
60
|
+
error: new Set(),
|
|
61
|
+
timeout: new Set(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let _timer = null;
|
|
65
|
+
let _timeoutTimer = null;
|
|
66
|
+
let _stopped = false;
|
|
67
|
+
let _count = 0;
|
|
68
|
+
|
|
69
|
+
function emit(event, p) {
|
|
70
|
+
for (const fn of listeners[event]) {
|
|
71
|
+
try {
|
|
72
|
+
fn(p);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function evalPredicate() {
|
|
80
|
+
if (typeof predicate === "function") {
|
|
81
|
+
return await predicate();
|
|
82
|
+
}
|
|
83
|
+
return Boolean(predicate);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function tick() {
|
|
87
|
+
if (_stopped) return;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const ok = await evalPredicate();
|
|
91
|
+
|
|
92
|
+
if (ok) {
|
|
93
|
+
_count++;
|
|
94
|
+
emit("trigger", payload);
|
|
95
|
+
|
|
96
|
+
if (max != null && _count >= max) {
|
|
97
|
+
_stopInternal();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!_stopped) {
|
|
103
|
+
_timer = setTimeout(tick, interval);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
emit("error", error);
|
|
107
|
+
if (!_stopped) {
|
|
108
|
+
_timer = setTimeout(tick, interval);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _stopInternal() {
|
|
114
|
+
_stopped = true;
|
|
115
|
+
if (_timer) {
|
|
116
|
+
clearTimeout(_timer);
|
|
117
|
+
_timer = null;
|
|
118
|
+
}
|
|
119
|
+
if (_timeoutTimer) {
|
|
120
|
+
clearTimeout(_timeoutTimer);
|
|
121
|
+
_timeoutTimer = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const _ = {
|
|
126
|
+
start() {
|
|
127
|
+
if (_stopped) return _;
|
|
128
|
+
|
|
129
|
+
if (!_timer) {
|
|
130
|
+
_timer = setTimeout(tick, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (timeout != null && !_timeoutTimer) {
|
|
134
|
+
_timeoutTimer = setTimeout(() => {
|
|
135
|
+
if (_stopped) return;
|
|
136
|
+
emit("timeout", payload);
|
|
137
|
+
_stopInternal();
|
|
138
|
+
}, timeout);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return _;
|
|
142
|
+
},
|
|
143
|
+
stop() {
|
|
144
|
+
_stopInternal();
|
|
145
|
+
return _;
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* @typedef { "error" | "trigger" | "timeout" } EventNames
|
|
149
|
+
* @typedef {{
|
|
150
|
+
* error: (error) => void|Promise<void>,
|
|
151
|
+
* trigger: (payload) => void|Promise<void>,
|
|
152
|
+
* timeout: (payload) => void|Promise<void>,
|
|
153
|
+
* }} EventHandlers
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* @template {EventNames} E
|
|
157
|
+
* @param {E} event
|
|
158
|
+
* @param {EventHandlers[E]} handler
|
|
159
|
+
*/
|
|
160
|
+
on(event, handler) {
|
|
161
|
+
listeners[event]?.add(handler);
|
|
162
|
+
return _;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return _;
|
|
167
|
+
},
|
|
168
|
+
dontCrash() {
|
|
169
|
+
const defaults = {
|
|
170
|
+
error: (error) => {
|
|
171
|
+
logger.log(`<% red [@ix-xs/node-comfort] Process Error %>\n<% gray ${error.stack ?? error} %>`);
|
|
172
|
+
},
|
|
173
|
+
exit: (code) => {
|
|
174
|
+
logger.log(`<% yellow [@ix-xs/node-comfort] Process Exit %>\n→ Code: <% gray ${code} %>`);
|
|
175
|
+
},
|
|
176
|
+
sig: (signal) => {
|
|
177
|
+
logger.log(`<% yellow [@ix-xs/node-comfort] Process SIG* %>\n→ Signal: <% gray ${signal} %>`);
|
|
178
|
+
process.exit(0);
|
|
179
|
+
},
|
|
180
|
+
beforeExit: (code) => {},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const _ = {
|
|
184
|
+
/**
|
|
185
|
+
* @template { "error" | "exit" | "sig" | "beforeExit" } E
|
|
186
|
+
* @param { E } event
|
|
187
|
+
* @param { { error: (error: Error | unknown) => void | Promise<void>, exit: (code: number) => void | Promise<void>, sig: (signal: "SIGINT" | "SIGTERM" | "SIGQUIT") => void | Promise<void>, beforeExit: (code: number) => void|Promise<void> }[E] } handler
|
|
188
|
+
*/
|
|
189
|
+
on(event, handler) {
|
|
190
|
+
if (event === "error") {
|
|
191
|
+
process.removeAllListeners("uncaughtException");
|
|
192
|
+
process.removeAllListeners("unhandledRejection");
|
|
193
|
+
process.on("uncaughtException", handler ?? defaults.error);
|
|
194
|
+
process.on("unhandledRejection", handler ?? defaults.error);
|
|
195
|
+
} else if (event === "exit") {
|
|
196
|
+
process.removeAllListeners("exit");
|
|
197
|
+
process.on("exit", handler ?? defaults.exit);
|
|
198
|
+
} else if (event === "sig") {
|
|
199
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGQUIT"]) {
|
|
200
|
+
process.removeAllListeners(signal);
|
|
201
|
+
process.on(signal, handler ?? defaults.sig);
|
|
202
|
+
}
|
|
203
|
+
} else if (event === "beforeExit") {
|
|
204
|
+
process.removeAllListeners("beforeExit");
|
|
205
|
+
process.on("beforeExit", handler ?? defaults.beforeExit);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return _;
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
_.on("error").on("exit").on("sig");
|
|
213
|
+
|
|
214
|
+
return _;
|
|
215
|
+
},
|
|
216
|
+
/**
|
|
217
|
+
* @param {*} value
|
|
218
|
+
*/
|
|
219
|
+
JSONString(value) {
|
|
220
|
+
return JSON.stringify(value, null, 4);
|
|
221
|
+
},
|
|
222
|
+
JSONParse(value) {
|
|
223
|
+
return JSON.parse(value);
|
|
224
|
+
},
|
|
225
|
+
};
|