@anonympins/fingerprint 0.2.0 → 0.2.2
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/README.md +60 -4
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +11 -22
- package/fingerprint.js +324 -355
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +89 -33
- package/pow.worker.js +27 -0
- package/problem-manager.js +247 -0
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { Optimization } from './library.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @namespace ProblemInitializers
|
|
6
|
+
* @description Fonctions pour générer dynamiquement les données d'un problème.
|
|
7
|
+
*/
|
|
8
|
+
const ProblemInitializers = {
|
|
9
|
+
/**
|
|
10
|
+
* Génère un ensemble de points aléatoires pour un problème de TSP.
|
|
11
|
+
* @param {object} params - Les paramètres de génération.
|
|
12
|
+
* @param {number} params.count - Le nombre de points à générer.
|
|
13
|
+
* @param {{x: number, y: number}} [params.bounds={x: 1000, y: 1000}] - Les limites spatiales.
|
|
14
|
+
* @returns {Array<{x: number, y: number}>}
|
|
15
|
+
*/
|
|
16
|
+
'generate:randomPoints': (params) => {
|
|
17
|
+
const { count, bounds = { x: 1000, y: 1000 } } = params;
|
|
18
|
+
if (isNaN(count)) return [];
|
|
19
|
+
return Array.from({ length: count }, () => ({ x: Math.random() * bounds.x, y: Math.random() * bounds.y }));
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Génère un ensemble d'actifs financiers aléatoires pour un problème de portefeuille.
|
|
24
|
+
* @param {object} params - Les paramètres de génération.
|
|
25
|
+
* @param {number} params.count - Le nombre d'actifs à générer.
|
|
26
|
+
* @returns {Array<{expectedReturn: number, volatility: number}>}
|
|
27
|
+
*/
|
|
28
|
+
'generate:randomAssets': (params) => {
|
|
29
|
+
const { count } = params;
|
|
30
|
+
if (isNaN(count)) return [];
|
|
31
|
+
return Array.from({ length: count }, () => ({
|
|
32
|
+
expectedReturn: Math.random() * 0.2,
|
|
33
|
+
volatility: 0.1 + Math.random() * 0.3
|
|
34
|
+
}));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Crée une fonction qui génère des arguments pour chaque worker de `runMultipleParallel`.
|
|
39
|
+
* Permet de faire varier les paramètres (ex: solution initiale) pour chaque cycle.
|
|
40
|
+
* @param {object} params - Les paramètres de configuration.
|
|
41
|
+
* @param {Array<any>} params.baseArgs - Les arguments de base, communs à tous les workers.
|
|
42
|
+
* @param {object} params.variations - Décrit comment faire varier un argument.
|
|
43
|
+
* @returns {function(number): Array<any>} La fonction `workerDataGenerator`.
|
|
44
|
+
*/
|
|
45
|
+
'generate:parallelArgs': (params) => {
|
|
46
|
+
const { baseArgs, variations } = params;
|
|
47
|
+
return (cycleIndex) => {
|
|
48
|
+
const cycleArgs = [...baseArgs];
|
|
49
|
+
// Pour l'instant, on gère la variation de la solution initiale pour le TSP
|
|
50
|
+
if (variations?.initialSolution === 'random') {
|
|
51
|
+
cycleArgs[0] = cycleArgs[0].sort(() => Math.random() - 0.5);
|
|
52
|
+
}
|
|
53
|
+
return cycleArgs;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
class ProblemManager {
|
|
59
|
+
constructor(configPath) {
|
|
60
|
+
this.configPath = configPath;
|
|
61
|
+
this.problems = this.loadProblems();
|
|
62
|
+
this.currentProblemIndex = 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loadProblems() {
|
|
66
|
+
try {
|
|
67
|
+
const data = readFileSync(this.configPath, 'utf-8');
|
|
68
|
+
const problems = JSON.parse(data);
|
|
69
|
+
// Initialisation dynamique des problèmes
|
|
70
|
+
for (const problem of problems) { // eslint-disable-line no-unused-vars
|
|
71
|
+
for (const key in problem.payload) {
|
|
72
|
+
const value = problem.payload[key];
|
|
73
|
+
// On cherche une instruction d'initialisation (ex: { "$init": "generate:randomPoints", ... })
|
|
74
|
+
if (typeof value === 'object' && value !== null && value.$init) {
|
|
75
|
+
const initializer = ProblemInitializers[value.$init];
|
|
76
|
+
if (initializer) {
|
|
77
|
+
// On remplace l'objet d'instruction par les données générées.
|
|
78
|
+
problem.payload[key] = initializer(value.params || {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return problems;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[ProblemManager] Erreur lors du chargement du fichier de problèmes: ${error.message}`);
|
|
86
|
+
return []; // Retourne un tableau vide en cas d'erreur pour éviter un crash
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
saveProblems() {
|
|
91
|
+
// Note: Dans un vrai scénario, utilisez une base de données pour éviter les race conditions.
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(this.configPath, JSON.stringify(this.problems, null, 2));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`[ProblemManager] Erreur lors de la sauvegarde du fichier de problèmes: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sélectionne un problème et génère une unité de travail.
|
|
101
|
+
* @param {number} suspicionFactor - Le facteur de suspicion pour ajuster la difficulté.
|
|
102
|
+
* @returns {{problemId: string, task: object}|null}
|
|
103
|
+
*/
|
|
104
|
+
dispatchWork(suspicionFactor) {
|
|
105
|
+
if (this.problems.length === 0) return null;
|
|
106
|
+
|
|
107
|
+
const problem = this.problems[this.currentProblemIndex];
|
|
108
|
+
this.currentProblemIndex = (this.currentProblemIndex + 1) % this.problems.length;
|
|
109
|
+
|
|
110
|
+
const task = { type: problem.workUnit.type };
|
|
111
|
+
const { scalingFactor } = problem.workUnit;
|
|
112
|
+
|
|
113
|
+
switch (problem.workUnit.type) {
|
|
114
|
+
case 'simulated_annealing_iterations':
|
|
115
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
116
|
+
const baseIterations = Math.max(15000, problem.workUnit.baseIterations || 0);
|
|
117
|
+
task.iterations = scalingFactor
|
|
118
|
+
? Math.floor(baseIterations * Math.pow(scalingFactor, suspicionFactor))
|
|
119
|
+
: Math.floor(baseIterations * (0.5 + suspicionFactor));
|
|
120
|
+
task.payload = problem.payload;
|
|
121
|
+
task.initialSolution = problem.state.bestSolution;
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
case 'genetic_algorithm_generations':
|
|
125
|
+
// Assurer une difficulté minimale pour que le challenge soit significatif
|
|
126
|
+
const baseGenerations = Math.max(50, problem.workUnit.baseGenerations || 0);
|
|
127
|
+
task.generations = scalingFactor
|
|
128
|
+
? Math.floor(baseGenerations * Math.pow(scalingFactor, suspicionFactor))
|
|
129
|
+
: Math.floor(baseGenerations * (0.5 + suspicionFactor));
|
|
130
|
+
task.payload = problem.payload;
|
|
131
|
+
task.initialPopulation = problem.state.population;
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'run_multiple_parallel':
|
|
135
|
+
task.solverName = problem.workUnit.solverName;
|
|
136
|
+
task.numCycles = problem.workUnit.numCycles;
|
|
137
|
+
// Les arguments et le générateur sont dans le payload pour plus de flexibilité
|
|
138
|
+
task.baseSolverArgs = problem.payload.baseSolverArgs;
|
|
139
|
+
task.workerDataGenerator = problem.payload.workerDataGenerator;
|
|
140
|
+
task.logProgress = problem.payload.logProgress || false;
|
|
141
|
+
task.concurrency = problem.payload.concurrency;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { problemId: problem.id, task };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Intègre la solution d'un client dans l'état du problème.
|
|
150
|
+
* @param {string} problemId - L'ID du problème.
|
|
151
|
+
* @param {object} solutionData - La solution renvoyée par le client.
|
|
152
|
+
*/
|
|
153
|
+
integrateSolution(problemId, solutionData) {
|
|
154
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
155
|
+
if (!problem) return;
|
|
156
|
+
|
|
157
|
+
switch (problem.workUnit.type) {
|
|
158
|
+
case 'simulated_annealing_iterations':
|
|
159
|
+
if (solutionData.energy < (parseFloat(problem.state.bestEnergy) || Infinity)) {
|
|
160
|
+
problem.state.bestSolution = solutionData.solution;
|
|
161
|
+
problem.state.bestEnergy = solutionData.energy;
|
|
162
|
+
console.log(`[ProblemManager] Nouvelle meilleure solution pour ${problemId}: ${solutionData.energy.toFixed(2)}`);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
case 'genetic_algorithm_generations':
|
|
166
|
+
problem.state.population = solutionData.population;
|
|
167
|
+
console.log(`[ProblemManager] Population mise à jour pour ${problemId}.`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
this.saveProblems();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* S'assure qu'un problème a une solution initiale. Si non, en génère une.
|
|
175
|
+
* @param {object} problem - L'objet problème.
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
_ensureInitialSolution(problem) {
|
|
179
|
+
if (problem.state.bestSolution) {
|
|
180
|
+
return; // Une solution existe déjà
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`[ProblemManager] Génération d'une solution initiale pour le problème ${problem.id}...`);
|
|
184
|
+
|
|
185
|
+
// On se base sur le type de problème pour générer une solution de base.
|
|
186
|
+
// Pour l'instant, on gère le cas le plus commun (TSP/points)
|
|
187
|
+
// qui utilise le recuit simulé.
|
|
188
|
+
switch (problem.workUnit.type) {
|
|
189
|
+
case 'simulated_annealing_iterations': {
|
|
190
|
+
// Pour un TSP, une solution initiale est un ordre des points.
|
|
191
|
+
// On prend l'ordre initial des points du payload.
|
|
192
|
+
const initialSolution = problem.payload.points;
|
|
193
|
+
if (initialSolution && Array.isArray(initialSolution)) {
|
|
194
|
+
// On calcule l'énergie (coût) de cette solution initiale.
|
|
195
|
+
const energy = Optimization.tsp.calculateEnergy(initialSolution);
|
|
196
|
+
problem.state.bestSolution = initialSolution;
|
|
197
|
+
problem.state.bestEnergy = energy;
|
|
198
|
+
problem.state.lastUpdate = new Date().toISOString();
|
|
199
|
+
console.log(`[ProblemManager] Solution initiale pour ${problem.id} générée avec une énergie de ${energy.toFixed(2)}.`);
|
|
200
|
+
this.saveProblems(); // On sauvegarde la nouvelle solution
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
// D'autres types de problèmes (ex: algo génétique) pourraient être ajoutés ici.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Récupère la meilleure solution actuellement connue pour un ou plusieurs problèmes.
|
|
210
|
+
* @param {string} [problemId] - L'ID optionnel du problème à consulter.
|
|
211
|
+
* Si non fourni, retourne les meilleures solutions pour tous les problèmes.
|
|
212
|
+
* @returns {object|Array<object>|null}
|
|
213
|
+
* - Si un `problemId` est fourni, retourne un objet `{ id, solution, score }` ou `null` si non trouvé.
|
|
214
|
+
* - Si aucun `problemId` n'est fourni, retourne un tableau de ces objets.
|
|
215
|
+
*/
|
|
216
|
+
getBestSolutions(problemId) {
|
|
217
|
+
const problemsToProcess = problemId
|
|
218
|
+
? this.problems.filter(p => p.id === problemId)
|
|
219
|
+
: this.problems;
|
|
220
|
+
|
|
221
|
+
problemsToProcess.forEach(p => this._ensureInitialSolution(p));
|
|
222
|
+
|
|
223
|
+
const formatSolution = (p) => {
|
|
224
|
+
// Après _ensureInitialSolution, on peut supposer que p.state existe.
|
|
225
|
+
if (!p || !p.state) return null;
|
|
226
|
+
return {
|
|
227
|
+
id: p.id,
|
|
228
|
+
solution: p.state.bestSolution,
|
|
229
|
+
// Gère les deux types de scores : 'energy' (recuit simulé) et 'fitness' (algo génétique)
|
|
230
|
+
score: p.state.bestEnergy !== undefined ? p.state.bestEnergy : p.state.bestFitness,
|
|
231
|
+
lastUpdate: p.state.lastUpdate,
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (problemId) {
|
|
236
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
237
|
+
return problem ? formatSolution(problem) : null; // Le filtrage initial a déjà fait le travail
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Retourne un aperçu pour tous les problèmes
|
|
241
|
+
return this.problems.map(formatSolution).filter(s => s && s.solution);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { ProblemManager }; // Export the class for testing
|
|
247
|
+
export const problemManager = new ProblemManager('./problems.config.json');
|
package/redis-store.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for ioredis.
|
|
3
|
-
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
-
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for an ioredis client.
|
|
9
|
-
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
-
*/
|
|
12
|
-
export function createRedisStore(redisClient) {
|
|
13
|
-
return {
|
|
14
|
-
async get(key) {
|
|
15
|
-
const value = await redisClient.get(key);
|
|
16
|
-
if (!value) return null;
|
|
17
|
-
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
-
return JSON.parse(value, (k, v) => {
|
|
19
|
-
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
-
return new Set(v);
|
|
21
|
-
}
|
|
22
|
-
return v;
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
async set(key, value, ttl) {
|
|
26
|
-
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
-
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
-
if (v instanceof Set) {
|
|
29
|
-
return Array.from(v);
|
|
30
|
-
}
|
|
31
|
-
return v;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (ttl && ttl > 0) {
|
|
35
|
-
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
-
} else {
|
|
37
|
-
await redisClient.set(key, stringValue);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
-
async delete(key) { await redisClient.del(key); },
|
|
42
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for ioredis.
|
|
3
|
+
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
+
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for an ioredis client.
|
|
9
|
+
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
+
*/
|
|
12
|
+
export function createRedisStore(redisClient) {
|
|
13
|
+
return {
|
|
14
|
+
async get(key) {
|
|
15
|
+
const value = await redisClient.get(key);
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
+
return JSON.parse(value, (k, v) => {
|
|
19
|
+
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
+
return new Set(v);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async set(key, value, ttl) {
|
|
26
|
+
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
+
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
+
if (v instanceof Set) {
|
|
29
|
+
return Array.from(v);
|
|
30
|
+
}
|
|
31
|
+
return v;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (ttl && ttl > 0) {
|
|
35
|
+
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
+
} else {
|
|
37
|
+
await redisClient.set(key, stringValue);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
+
async delete(key) { await redisClient.del(key); },
|
|
42
|
+
};
|
|
43
43
|
}
|
package/sql-store.js
CHANGED
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for Knex.js.
|
|
3
|
-
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a Knex.js client.
|
|
9
|
-
*
|
|
10
|
-
* **Note:** You must create the table yourself before using the store.
|
|
11
|
-
* The table should have at least the following columns:
|
|
12
|
-
* - `key` (string, primary key)
|
|
13
|
-
* - `value` (text or json/jsonb)
|
|
14
|
-
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
-
*
|
|
16
|
-
* Example schema for PostgreSQL:
|
|
17
|
-
* ```sql
|
|
18
|
-
* CREATE TABLE your_table_name (
|
|
19
|
-
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
-
* "value" TEXT NOT NULL,
|
|
21
|
-
* "expiresAt" TIMESTAMPTZ
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
-
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
-
*/
|
|
29
|
-
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
-
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
async get(key) {
|
|
36
|
-
const row = await knex(tableName).where('key', key).first();
|
|
37
|
-
if (!row) return null;
|
|
38
|
-
|
|
39
|
-
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
-
await this.delete(key); // Clean up expired key.
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(row.value, reviver);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// In case of malformed JSON, treat it as a miss.
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async set(key, value, ttl) {
|
|
54
|
-
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
-
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
-
|
|
57
|
-
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
-
await knex(tableName)
|
|
59
|
-
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
-
.onConflict('key')
|
|
61
|
-
.merge();
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
async has(key) {
|
|
65
|
-
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
-
if (!row) return false;
|
|
67
|
-
// Also check for expiration here.
|
|
68
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async delete(key) {
|
|
75
|
-
await knex(tableName).where('key', key).del();
|
|
76
|
-
},
|
|
77
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for Knex.js.
|
|
3
|
+
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
+
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a Knex.js client.
|
|
9
|
+
*
|
|
10
|
+
* **Note:** You must create the table yourself before using the store.
|
|
11
|
+
* The table should have at least the following columns:
|
|
12
|
+
* - `key` (string, primary key)
|
|
13
|
+
* - `value` (text or json/jsonb)
|
|
14
|
+
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
+
*
|
|
16
|
+
* Example schema for PostgreSQL:
|
|
17
|
+
* ```sql
|
|
18
|
+
* CREATE TABLE your_table_name (
|
|
19
|
+
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
+
* "value" TEXT NOT NULL,
|
|
21
|
+
* "expiresAt" TIMESTAMPTZ
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
+
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
+
*/
|
|
29
|
+
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
+
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async get(key) {
|
|
36
|
+
const row = await knex(tableName).where('key', key).first();
|
|
37
|
+
if (!row) return null;
|
|
38
|
+
|
|
39
|
+
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
+
await this.delete(key); // Clean up expired key.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(row.value, reviver);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// In case of malformed JSON, treat it as a miss.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async set(key, value, ttl) {
|
|
54
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
+
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
+
|
|
57
|
+
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
+
await knex(tableName)
|
|
59
|
+
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
+
.onConflict('key')
|
|
61
|
+
.merge();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async has(key) {
|
|
65
|
+
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
+
if (!row) return false;
|
|
67
|
+
// Also check for expiration here.
|
|
68
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await knex(tableName).where('key', key).del();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
78
|
}
|