canvasframework 0.5.18 → 0.5.20
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 +30 -0
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- package/dist/canvasframework.js.LICENSE.txt +0 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gestionnaire de cryptographie pour le Canvas Framework
|
|
3
|
+
* Utilise l'API Web Crypto (SubtleCrypto) pour un chiffrement sécurisé
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
class CryptoManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Vérifier la disponibilité de l'API Crypto
|
|
9
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
10
|
+
throw new Error('Web Crypto API is not available in this browser');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.crypto = window.crypto.subtle;
|
|
14
|
+
|
|
15
|
+
// Algorithme par défaut
|
|
16
|
+
this.algorithm = {
|
|
17
|
+
name: 'AES-GCM',
|
|
18
|
+
length: 256
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Stockage des clés en mémoire (pas dans localStorage !)
|
|
22
|
+
this._keys = new Map();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Génère une clé de chiffrement aléatoire
|
|
27
|
+
* @param {string} keyName - Nom de la clé pour référence ultérieure
|
|
28
|
+
* @returns {Promise<CryptoKey>}
|
|
29
|
+
*/
|
|
30
|
+
async generateKey(keyName = 'default') {
|
|
31
|
+
const key = await this.crypto.generateKey(
|
|
32
|
+
{
|
|
33
|
+
name: this.algorithm.name,
|
|
34
|
+
length: this.algorithm.length
|
|
35
|
+
},
|
|
36
|
+
true, // extractable
|
|
37
|
+
['encrypt', 'decrypt']
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this._keys.set(keyName, key);
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Importe une clé depuis une chaîne (base64)
|
|
46
|
+
* @param {string} keyString - Clé au format base64
|
|
47
|
+
* @param {string} keyName - Nom de la clé
|
|
48
|
+
* @returns {Promise<CryptoKey>}
|
|
49
|
+
*/
|
|
50
|
+
async importKey(keyString, keyName = 'default') {
|
|
51
|
+
const keyBuffer = this._base64ToBuffer(keyString);
|
|
52
|
+
|
|
53
|
+
const key = await this.crypto.importKey(
|
|
54
|
+
'raw',
|
|
55
|
+
keyBuffer,
|
|
56
|
+
{ name: this.algorithm.name },
|
|
57
|
+
true,
|
|
58
|
+
['encrypt', 'decrypt']
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
this._keys.set(keyName, key);
|
|
62
|
+
return key;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Exporte une clé au format base64
|
|
67
|
+
* @param {string} keyName - Nom de la clé
|
|
68
|
+
* @returns {Promise<string>}
|
|
69
|
+
*/
|
|
70
|
+
async exportKey(keyName = 'default') {
|
|
71
|
+
const key = this._keys.get(keyName);
|
|
72
|
+
if (!key) {
|
|
73
|
+
throw new Error(`Key "${keyName}" not found`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const exported = await this.crypto.exportKey('raw', key);
|
|
77
|
+
return this._bufferToBase64(exported);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Dérive une clé depuis un mot de passe
|
|
82
|
+
* @param {string} password - Mot de passe
|
|
83
|
+
* @param {string} keyName - Nom de la clé
|
|
84
|
+
* @param {string} salt - Salt (optionnel, généré automatiquement)
|
|
85
|
+
* @returns {Promise<{key: CryptoKey, salt: string}>}
|
|
86
|
+
*/
|
|
87
|
+
async deriveKeyFromPassword(password, keyName = 'default', salt = null) {
|
|
88
|
+
// Générer ou utiliser le salt fourni
|
|
89
|
+
const saltBuffer = salt
|
|
90
|
+
? this._base64ToBuffer(salt)
|
|
91
|
+
: window.crypto.getRandomValues(new Uint8Array(16));
|
|
92
|
+
|
|
93
|
+
// Encoder le mot de passe
|
|
94
|
+
const encoder = new TextEncoder();
|
|
95
|
+
const passwordBuffer = encoder.encode(password);
|
|
96
|
+
|
|
97
|
+
// Importer le mot de passe comme clé
|
|
98
|
+
const keyMaterial = await this.crypto.importKey(
|
|
99
|
+
'raw',
|
|
100
|
+
passwordBuffer,
|
|
101
|
+
{ name: 'PBKDF2' },
|
|
102
|
+
false,
|
|
103
|
+
['deriveBits', 'deriveKey']
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Dériver la clé
|
|
107
|
+
const key = await this.crypto.deriveKey(
|
|
108
|
+
{
|
|
109
|
+
name: 'PBKDF2',
|
|
110
|
+
salt: saltBuffer,
|
|
111
|
+
iterations: 100000,
|
|
112
|
+
hash: 'SHA-256'
|
|
113
|
+
},
|
|
114
|
+
keyMaterial,
|
|
115
|
+
{ name: this.algorithm.name, length: this.algorithm.length },
|
|
116
|
+
true,
|
|
117
|
+
['encrypt', 'decrypt']
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
this._keys.set(keyName, key);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
key,
|
|
124
|
+
salt: this._bufferToBase64(saltBuffer)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Chiffre des données
|
|
130
|
+
* @param {any} data - Données à chiffrer (sera converti en JSON)
|
|
131
|
+
* @param {string} keyName - Nom de la clé à utiliser
|
|
132
|
+
* @returns {Promise<{encrypted: string, iv: string}>}
|
|
133
|
+
*/
|
|
134
|
+
async encrypt(data, keyName = 'default') {
|
|
135
|
+
const key = this._keys.get(keyName);
|
|
136
|
+
if (!key) {
|
|
137
|
+
throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Convertir les données en JSON puis en buffer
|
|
141
|
+
const dataString = typeof data === 'string' ? data : JSON.stringify(data);
|
|
142
|
+
const encoder = new TextEncoder();
|
|
143
|
+
const dataBuffer = encoder.encode(dataString);
|
|
144
|
+
|
|
145
|
+
// Générer un IV (Initialization Vector) aléatoire
|
|
146
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
147
|
+
|
|
148
|
+
// Chiffrer
|
|
149
|
+
const encryptedBuffer = await this.crypto.encrypt(
|
|
150
|
+
{
|
|
151
|
+
name: this.algorithm.name,
|
|
152
|
+
iv: iv
|
|
153
|
+
},
|
|
154
|
+
key,
|
|
155
|
+
dataBuffer
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
encrypted: this._bufferToBase64(encryptedBuffer),
|
|
160
|
+
iv: this._bufferToBase64(iv)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Déchiffre des données
|
|
166
|
+
* @param {string} encryptedData - Données chiffrées (base64)
|
|
167
|
+
* @param {string} iv - IV utilisé lors du chiffrement (base64)
|
|
168
|
+
* @param {string} keyName - Nom de la clé à utiliser
|
|
169
|
+
* @param {boolean} parseJson - Parser le résultat en JSON (true par défaut)
|
|
170
|
+
* @returns {Promise<any>}
|
|
171
|
+
*/
|
|
172
|
+
async decrypt(encryptedData, iv, keyName = 'default', parseJson = true) {
|
|
173
|
+
const key = this._keys.get(keyName);
|
|
174
|
+
if (!key) {
|
|
175
|
+
throw new Error(`Key "${keyName}" not found. Generate or import a key first.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Convertir les chaînes base64 en buffers
|
|
179
|
+
const encryptedBuffer = this._base64ToBuffer(encryptedData);
|
|
180
|
+
const ivBuffer = this._base64ToBuffer(iv);
|
|
181
|
+
|
|
182
|
+
// Déchiffrer
|
|
183
|
+
const decryptedBuffer = await this.crypto.decrypt(
|
|
184
|
+
{
|
|
185
|
+
name: this.algorithm.name,
|
|
186
|
+
iv: ivBuffer
|
|
187
|
+
},
|
|
188
|
+
key,
|
|
189
|
+
encryptedBuffer
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Convertir le buffer en string
|
|
193
|
+
const decoder = new TextDecoder();
|
|
194
|
+
const decryptedString = decoder.decode(decryptedBuffer);
|
|
195
|
+
|
|
196
|
+
// Parser en JSON si demandé
|
|
197
|
+
if (parseJson) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(decryptedString);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Si le parsing échoue, retourner la chaîne brute
|
|
202
|
+
return decryptedString;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return decryptedString;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Chiffre et encode en une seule chaîne (pratique pour stockage)
|
|
211
|
+
* @param {any} data - Données à chiffrer
|
|
212
|
+
* @param {string} keyName - Nom de la clé
|
|
213
|
+
* @returns {Promise<string>} - Chaîne contenant données chiffrées + IV
|
|
214
|
+
*/
|
|
215
|
+
async encryptToString(data, keyName = 'default') {
|
|
216
|
+
const { encrypted, iv } = await this.encrypt(data, keyName);
|
|
217
|
+
return `${encrypted}.${iv}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Déchiffre depuis une chaîne créée par encryptToString
|
|
222
|
+
* @param {string} encryptedString - Chaîne chiffrée
|
|
223
|
+
* @param {string} keyName - Nom de la clé
|
|
224
|
+
* @param {boolean} parseJson - Parser en JSON
|
|
225
|
+
* @returns {Promise<any>}
|
|
226
|
+
*/
|
|
227
|
+
async decryptFromString(encryptedString, keyName = 'default', parseJson = true) {
|
|
228
|
+
const [encrypted, iv] = encryptedString.split('.');
|
|
229
|
+
if (!encrypted || !iv) {
|
|
230
|
+
throw new Error('Invalid encrypted string format');
|
|
231
|
+
}
|
|
232
|
+
return this.decrypt(encrypted, iv, keyName, parseJson);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hash une chaîne (non réversible)
|
|
237
|
+
* @param {string} data - Données à hasher
|
|
238
|
+
* @param {string} algorithm - Algorithme (SHA-256, SHA-384, SHA-512)
|
|
239
|
+
* @returns {Promise<string>} - Hash en base64
|
|
240
|
+
*/
|
|
241
|
+
async hash(data, algorithm = 'SHA-256') {
|
|
242
|
+
const encoder = new TextEncoder();
|
|
243
|
+
const dataBuffer = encoder.encode(data);
|
|
244
|
+
|
|
245
|
+
const hashBuffer = await this.crypto.digest(algorithm, dataBuffer);
|
|
246
|
+
return this._bufferToBase64(hashBuffer);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Génère un token aléatoire sécurisé
|
|
251
|
+
* @param {number} length - Longueur en octets (32 par défaut)
|
|
252
|
+
* @returns {string} - Token en base64
|
|
253
|
+
*/
|
|
254
|
+
generateToken(length = 32) {
|
|
255
|
+
const buffer = window.crypto.getRandomValues(new Uint8Array(length));
|
|
256
|
+
return this._bufferToBase64(buffer);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Supprime une clé de la mémoire
|
|
261
|
+
* @param {string} keyName - Nom de la clé
|
|
262
|
+
*/
|
|
263
|
+
deleteKey(keyName) {
|
|
264
|
+
this._keys.delete(keyName);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Supprime toutes les clés
|
|
269
|
+
*/
|
|
270
|
+
deleteAllKeys() {
|
|
271
|
+
this._keys.clear();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Liste les clés disponibles
|
|
276
|
+
* @returns {string[]}
|
|
277
|
+
*/
|
|
278
|
+
listKeys() {
|
|
279
|
+
return Array.from(this._keys.keys());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ===== Méthodes utilitaires =====
|
|
283
|
+
|
|
284
|
+
_bufferToBase64(buffer) {
|
|
285
|
+
const bytes = new Uint8Array(buffer);
|
|
286
|
+
let binary = '';
|
|
287
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
288
|
+
binary += String.fromCharCode(bytes[i]);
|
|
289
|
+
}
|
|
290
|
+
return btoa(binary);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_base64ToBuffer(base64) {
|
|
294
|
+
const binary = atob(base64);
|
|
295
|
+
const bytes = new Uint8Array(binary.length);
|
|
296
|
+
for (let i = 0; i < binary.length; i++) {
|
|
297
|
+
bytes[i] = binary.charCodeAt(i);
|
|
298
|
+
}
|
|
299
|
+
return bytes.buffer;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export default CryptoManager;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache local avec TTL (Time To Live) et gestion avancée
|
|
3
|
+
* @class
|
|
4
|
+
* @property {Map} store - Stockage interne
|
|
5
|
+
* @property {number} defaultTTL - TTL par défaut en ms
|
|
6
|
+
* @property {number} maxSize - Taille max du cache
|
|
7
|
+
* @property {Object} stats - Statistiques du cache
|
|
8
|
+
*/
|
|
9
|
+
class DataStore {
|
|
10
|
+
/**
|
|
11
|
+
* Crée une instance de DataStore
|
|
12
|
+
* @param {Object} [options={}] - Options
|
|
13
|
+
* @param {number} [options.defaultTTL=3600000] - TTL par défaut (1h)
|
|
14
|
+
* @param {number} [options.maxSize=100] - Taille max du cache
|
|
15
|
+
* @param {boolean} [options.enableStats=true] - Activer les stats
|
|
16
|
+
*/
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.store = new Map();
|
|
19
|
+
this.defaultTTL = options.defaultTTL || 3600000; // 1 heure par défaut
|
|
20
|
+
this.maxSize = options.maxSize || 100;
|
|
21
|
+
this.enableStats = options.enableStats !== false;
|
|
22
|
+
|
|
23
|
+
// Statistiques
|
|
24
|
+
this.stats = {
|
|
25
|
+
hits: 0,
|
|
26
|
+
misses: 0,
|
|
27
|
+
sets: 0,
|
|
28
|
+
deletes: 0,
|
|
29
|
+
evictions: 0
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Nettoyage périodique
|
|
33
|
+
this.startCleanupInterval();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stocke une valeur avec TTL
|
|
38
|
+
* @param {string} key - Clé
|
|
39
|
+
* @param {*} value - Valeur
|
|
40
|
+
* @param {number} [ttl] - TTL en ms (optionnel)
|
|
41
|
+
* @returns {DataStore} Instance pour chaînage
|
|
42
|
+
*/
|
|
43
|
+
set(key, value, ttl) {
|
|
44
|
+
const expiresAt = Date.now() + (ttl || this.defaultTTL);
|
|
45
|
+
|
|
46
|
+
// Si le cache est plein, supprimer l'élément le plus ancien
|
|
47
|
+
if (this.store.size >= this.maxSize && !this.store.has(key)) {
|
|
48
|
+
this.evictOldest();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.store.set(key, {
|
|
52
|
+
value,
|
|
53
|
+
expiresAt,
|
|
54
|
+
createdAt: Date.now(),
|
|
55
|
+
accessCount: 0
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (this.enableStats) this.stats.sets++;
|
|
59
|
+
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Récupère une valeur
|
|
65
|
+
* @param {string} key - Clé
|
|
66
|
+
* @param {*} [defaultValue=null] - Valeur par défaut
|
|
67
|
+
* @returns {*} Valeur ou null
|
|
68
|
+
*/
|
|
69
|
+
get(key, defaultValue = null) {
|
|
70
|
+
const item = this.store.get(key);
|
|
71
|
+
|
|
72
|
+
if (!item) {
|
|
73
|
+
if (this.enableStats) this.stats.misses++;
|
|
74
|
+
return defaultValue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Vérifier l'expiration
|
|
78
|
+
if (Date.now() > item.expiresAt) {
|
|
79
|
+
this.delete(key);
|
|
80
|
+
if (this.enableStats) this.stats.misses++;
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mettre à jour les stats d'accès
|
|
85
|
+
item.accessCount++;
|
|
86
|
+
|
|
87
|
+
if (this.enableStats) this.stats.hits++;
|
|
88
|
+
|
|
89
|
+
return item.value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Vérifie si une clé existe et n'est pas expirée
|
|
94
|
+
* @param {string} key - Clé
|
|
95
|
+
* @returns {boolean} True si existe et valide
|
|
96
|
+
*/
|
|
97
|
+
has(key) {
|
|
98
|
+
const item = this.store.get(key);
|
|
99
|
+
|
|
100
|
+
if (!item) return false;
|
|
101
|
+
|
|
102
|
+
if (Date.now() > item.expiresAt) {
|
|
103
|
+
this.delete(key);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Supprime une entrée
|
|
112
|
+
* @param {string} key - Clé
|
|
113
|
+
* @returns {boolean} True si supprimé
|
|
114
|
+
*/
|
|
115
|
+
delete(key) {
|
|
116
|
+
const deleted = this.store.delete(key);
|
|
117
|
+
if (deleted && this.enableStats) this.stats.deletes++;
|
|
118
|
+
return deleted;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Vide tout le cache
|
|
123
|
+
*/
|
|
124
|
+
clear() {
|
|
125
|
+
this.store.clear();
|
|
126
|
+
if (this.enableStats) {
|
|
127
|
+
this.stats = {
|
|
128
|
+
hits: 0,
|
|
129
|
+
misses: 0,
|
|
130
|
+
sets: 0,
|
|
131
|
+
deletes: 0,
|
|
132
|
+
evictions: 0
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Récupère ou calcule une valeur (memoization)
|
|
139
|
+
* @param {string} key - Clé
|
|
140
|
+
* @param {Function} factory - Fonction qui retourne la valeur
|
|
141
|
+
* @param {number} [ttl] - TTL optionnel
|
|
142
|
+
* @returns {Promise<*>} Valeur
|
|
143
|
+
*/
|
|
144
|
+
async getOrSet(key, factory, ttl) {
|
|
145
|
+
if (this.has(key)) {
|
|
146
|
+
return this.get(key);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const value = await factory();
|
|
150
|
+
this.set(key, value, ttl);
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Récupère plusieurs valeurs
|
|
156
|
+
* @param {Array<string>} keys - Clés
|
|
157
|
+
* @returns {Object} Objet clé-valeur
|
|
158
|
+
*/
|
|
159
|
+
getMany(keys) {
|
|
160
|
+
const result = {};
|
|
161
|
+
|
|
162
|
+
for (let key of keys) {
|
|
163
|
+
const value = this.get(key);
|
|
164
|
+
if (value !== null) {
|
|
165
|
+
result[key] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stocke plusieurs valeurs
|
|
174
|
+
* @param {Object} items - Objet clé-valeur
|
|
175
|
+
* @param {number} [ttl] - TTL optionnel
|
|
176
|
+
* @returns {DataStore} Instance pour chaînage
|
|
177
|
+
*/
|
|
178
|
+
setMany(items, ttl) {
|
|
179
|
+
for (let key in items) {
|
|
180
|
+
this.set(key, items[key], ttl);
|
|
181
|
+
}
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Supprime plusieurs entrées
|
|
187
|
+
* @param {Array<string>} keys - Clés
|
|
188
|
+
* @returns {number} Nombre de suppressions
|
|
189
|
+
*/
|
|
190
|
+
deleteMany(keys) {
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (let key of keys) {
|
|
193
|
+
if (this.delete(key)) count++;
|
|
194
|
+
}
|
|
195
|
+
return count;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Met à jour le TTL d'une entrée
|
|
200
|
+
* @param {string} key - Clé
|
|
201
|
+
* @param {number} ttl - Nouveau TTL en ms
|
|
202
|
+
* @returns {boolean} True si mis à jour
|
|
203
|
+
*/
|
|
204
|
+
touch(key, ttl) {
|
|
205
|
+
const item = this.store.get(key);
|
|
206
|
+
|
|
207
|
+
if (!item) return false;
|
|
208
|
+
|
|
209
|
+
item.expiresAt = Date.now() + (ttl || this.defaultTTL);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Obtient le TTL restant
|
|
215
|
+
* @param {string} key - Clé
|
|
216
|
+
* @returns {number} TTL en ms, ou -1 si n'existe pas
|
|
217
|
+
*/
|
|
218
|
+
ttl(key) {
|
|
219
|
+
const item = this.store.get(key);
|
|
220
|
+
|
|
221
|
+
if (!item) return -1;
|
|
222
|
+
|
|
223
|
+
const remaining = item.expiresAt - Date.now();
|
|
224
|
+
return remaining > 0 ? remaining : -1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Évince l'élément le plus ancien (LRU)
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
evictOldest() {
|
|
232
|
+
let oldestKey = null;
|
|
233
|
+
let oldestTime = Infinity;
|
|
234
|
+
|
|
235
|
+
for (let [key, item] of this.store.entries()) {
|
|
236
|
+
// Utiliser l'accès le moins récent comme critère
|
|
237
|
+
const priority = item.createdAt - (item.accessCount * 1000);
|
|
238
|
+
|
|
239
|
+
if (priority < oldestTime) {
|
|
240
|
+
oldestTime = priority;
|
|
241
|
+
oldestKey = key;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (oldestKey) {
|
|
246
|
+
this.store.delete(oldestKey);
|
|
247
|
+
if (this.enableStats) this.stats.evictions++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Nettoie les entrées expirées
|
|
253
|
+
* @returns {number} Nombre d'entrées nettoyées
|
|
254
|
+
*/
|
|
255
|
+
cleanup() {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
let cleaned = 0;
|
|
258
|
+
|
|
259
|
+
for (let [key, item] of this.store.entries()) {
|
|
260
|
+
if (now > item.expiresAt) {
|
|
261
|
+
this.store.delete(key);
|
|
262
|
+
cleaned++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return cleaned;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Démarre le nettoyage automatique
|
|
271
|
+
* @param {number} [interval=60000] - Intervalle en ms (1 minute)
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
startCleanupInterval(interval = 60000) {
|
|
275
|
+
if (this.cleanupTimer) {
|
|
276
|
+
clearInterval(this.cleanupTimer);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.cleanupTimer = setInterval(() => {
|
|
280
|
+
this.cleanup();
|
|
281
|
+
}, interval);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Arrête le nettoyage automatique
|
|
286
|
+
*/
|
|
287
|
+
stopCleanupInterval() {
|
|
288
|
+
if (this.cleanupTimer) {
|
|
289
|
+
clearInterval(this.cleanupTimer);
|
|
290
|
+
this.cleanupTimer = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Obtient toutes les clés
|
|
296
|
+
* @param {boolean} [includeExpired=false] - Inclure les expirées
|
|
297
|
+
* @returns {Array<string>} Liste des clés
|
|
298
|
+
*/
|
|
299
|
+
keys(includeExpired = false) {
|
|
300
|
+
const keys = [];
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
|
|
303
|
+
for (let [key, item] of this.store.entries()) {
|
|
304
|
+
if (includeExpired || now <= item.expiresAt) {
|
|
305
|
+
keys.push(key);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return keys;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Obtient la taille du cache
|
|
314
|
+
* @returns {number} Nombre d'entrées
|
|
315
|
+
*/
|
|
316
|
+
size() {
|
|
317
|
+
return this.store.size;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Obtient les statistiques
|
|
322
|
+
* @returns {Object} Statistiques
|
|
323
|
+
*/
|
|
324
|
+
getStats() {
|
|
325
|
+
const hitRate = this.stats.hits + this.stats.misses > 0
|
|
326
|
+
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
|
|
327
|
+
: 0;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
...this.stats,
|
|
331
|
+
size: this.store.size,
|
|
332
|
+
hitRate: `${hitRate}%`,
|
|
333
|
+
maxSize: this.maxSize
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Réinitialise les statistiques
|
|
339
|
+
*/
|
|
340
|
+
resetStats() {
|
|
341
|
+
this.stats = {
|
|
342
|
+
hits: 0,
|
|
343
|
+
misses: 0,
|
|
344
|
+
sets: 0,
|
|
345
|
+
deletes: 0,
|
|
346
|
+
evictions: 0
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Exporte le cache en JSON
|
|
352
|
+
* @returns {string} JSON
|
|
353
|
+
*/
|
|
354
|
+
export() {
|
|
355
|
+
const data = {};
|
|
356
|
+
|
|
357
|
+
for (let [key, item] of this.store.entries()) {
|
|
358
|
+
data[key] = {
|
|
359
|
+
value: item.value,
|
|
360
|
+
expiresAt: item.expiresAt,
|
|
361
|
+
createdAt: item.createdAt
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return JSON.stringify(data);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Importe un cache depuis JSON
|
|
370
|
+
* @param {string} json - JSON
|
|
371
|
+
*/
|
|
372
|
+
import(json) {
|
|
373
|
+
const data = JSON.parse(json);
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
|
|
376
|
+
for (let key in data) {
|
|
377
|
+
const item = data[key];
|
|
378
|
+
|
|
379
|
+
// Ne pas importer les entrées expirées
|
|
380
|
+
if (item.expiresAt > now) {
|
|
381
|
+
this.store.set(key, {
|
|
382
|
+
value: item.value,
|
|
383
|
+
expiresAt: item.expiresAt,
|
|
384
|
+
createdAt: item.createdAt,
|
|
385
|
+
accessCount: 0
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Détruit le store
|
|
393
|
+
*/
|
|
394
|
+
destroy() {
|
|
395
|
+
this.stopCleanupInterval();
|
|
396
|
+
this.clear();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Instance globale par défaut
|
|
401
|
+
DataStore.global = new DataStore();
|
|
402
|
+
|
|
403
|
+
export default DataStore;
|