@vipros-org/sdk 3.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/README.md +61 -0
- package/package.json +32 -0
- package/vipros-sdk.css +676 -0
- package/vipros-sdk.js +4190 -0
- package/vipros-sdk.min.css +676 -0
- package/vipros-sdk.min.js +2 -0
package/vipros-sdk.js
ADDED
|
@@ -0,0 +1,4190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIPros SDK v3.0.0-unified - Production Build
|
|
3
|
+
* Generated: 2026-02-10T11:29:53.281Z
|
|
4
|
+
* Environment: Production
|
|
5
|
+
*/
|
|
6
|
+
var ViprosSDK = (function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TemplateLoader - Gestionnaire de templates HTML modulaires
|
|
11
|
+
*
|
|
12
|
+
* Charge et met en cache les templates HTML externes pour le composant ViprosOfferCard.
|
|
13
|
+
* Utilise un système de templating simple avec remplacement de variables {{variable}}.
|
|
14
|
+
*/
|
|
15
|
+
class TemplateLoader {
|
|
16
|
+
constructor(debug = false) {
|
|
17
|
+
this.debug = debug;
|
|
18
|
+
this.templates = new Map();
|
|
19
|
+
this.baseUrl = this.detectBaseUrl();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Détecte l'URL de base pour les templates
|
|
24
|
+
*/
|
|
25
|
+
detectBaseUrl() {
|
|
26
|
+
// Toujours utiliser l'URL API pour les templates
|
|
27
|
+
const baseUrl = window.location.origin;
|
|
28
|
+
|
|
29
|
+
if (window.location.hostname.includes('ddev.site')) {
|
|
30
|
+
return baseUrl + '/api/sdk/components/templates'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pour production, adapter selon le domaine
|
|
34
|
+
return baseUrl + '/api/sdk/components/templates'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Charge un template depuis le serveur
|
|
39
|
+
*/
|
|
40
|
+
async loadTemplate(templateName) {
|
|
41
|
+
if (this.templates.has(templateName)) {
|
|
42
|
+
return this.templates.get(templateName)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${this.baseUrl}/${templateName}.html`);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Template ${templateName} not found: ${response.status}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const html = await response.text();
|
|
52
|
+
this.templates.set(templateName, html);
|
|
53
|
+
return html
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`[TemplateLoader] Error loading template ${templateName}:`, error);
|
|
56
|
+
return this.getFallbackTemplate(templateName)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Rend un template avec des données
|
|
62
|
+
*/
|
|
63
|
+
async render(templateName, data = {}) {
|
|
64
|
+
const template = await this.loadTemplate(templateName);
|
|
65
|
+
return this.interpolate(template, data)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Système de templating amélioré - gère {{#if}}, {{else}}, {{/if}}, {{#unless}}
|
|
70
|
+
*/
|
|
71
|
+
interpolate(template, data) {
|
|
72
|
+
// D'abord traiter les blocs conditionnels
|
|
73
|
+
let processed = this.processConditionalBlocks(template, data);
|
|
74
|
+
|
|
75
|
+
// Puis remplacer les variables simples
|
|
76
|
+
processed = processed.replace(/\{\{([^}#/][^}]*)\}\}/g, (match, key) => {
|
|
77
|
+
const value = this.resolveNestedKey(data, key.trim());
|
|
78
|
+
return value !== null ? value : ''
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return processed
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Résout les clés imbriquées comme "cashback.value"
|
|
86
|
+
*/
|
|
87
|
+
resolveNestedKey(obj, key) {
|
|
88
|
+
// Les conditions sont maintenant gérées par processConditionalBlocks
|
|
89
|
+
// Ignorer les tokens de condition restants
|
|
90
|
+
if (key.startsWith('#') || key.startsWith('/')) {
|
|
91
|
+
return ''
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Résolution de clé normale
|
|
95
|
+
return key.split('.').reduce((current, prop) => {
|
|
96
|
+
return current && current[prop] !== undefined ? current[prop] : null
|
|
97
|
+
}, obj)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Traite les blocs conditionnels {{#if}}...{{else}}...{{/if}} et {{#unless}}...{{/unless}}
|
|
102
|
+
* Gère les conditions imbriquées de façon récursive
|
|
103
|
+
*/
|
|
104
|
+
processConditionalBlocks(template, data) {
|
|
105
|
+
let processed = template;
|
|
106
|
+
let iterations = 0;
|
|
107
|
+
const maxIterations = 10; // Protection contre les boucles infinies
|
|
108
|
+
|
|
109
|
+
// Traiter récursivement jusqu'à ce qu'il n'y ait plus de conditions
|
|
110
|
+
while (processed.includes('{{#if') || processed.includes('{{#unless')) {
|
|
111
|
+
iterations++;
|
|
112
|
+
if (iterations > maxIterations) {
|
|
113
|
+
console.warn('[TemplateLoader] Too many iterations in conditional processing');
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const before = processed;
|
|
118
|
+
|
|
119
|
+
// Traiter les blocs {{#if}}...{{/if}} (de l'intérieur vers l'extérieur)
|
|
120
|
+
processed = this.processIfBlocks(processed, data);
|
|
121
|
+
|
|
122
|
+
// Traiter les blocs {{#unless}}...{{/unless}}
|
|
123
|
+
processed = this.processUnlessBlocks(processed, data);
|
|
124
|
+
|
|
125
|
+
// Si rien n'a changé, arrêter pour éviter une boucle infinie
|
|
126
|
+
if (processed === before) {
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return processed
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Traite les blocs {{#if}}...{{/if}} en gérant les conditions imbriquées
|
|
136
|
+
*/
|
|
137
|
+
processIfBlocks(template, data) {
|
|
138
|
+
// Trouver le {{#if}} le plus imbriqué (sans autres {{#if}} à l'intérieur)
|
|
139
|
+
const ifPattern = /\{\{#if\s+([^}]+)\}\}((?:(?!\{\{#if)[\s\S])*?)\{\{\/if\}\}/g;
|
|
140
|
+
|
|
141
|
+
return template.replace(ifPattern, (match, condition, content) => {
|
|
142
|
+
const value = this.resolveNestedKey(data, condition.trim());
|
|
143
|
+
const isTrue = Boolean(value);
|
|
144
|
+
|
|
145
|
+
// Chercher {{else}} dans le contenu (mais pas dans des sous-conditions)
|
|
146
|
+
const elseParts = content.split(/\{\{else\}\}/);
|
|
147
|
+
|
|
148
|
+
if (elseParts.length === 2) {
|
|
149
|
+
// Il y a un {{else}}
|
|
150
|
+
return isTrue ? elseParts[0] : elseParts[1]
|
|
151
|
+
} else {
|
|
152
|
+
// Pas de {{else}}, juste la condition
|
|
153
|
+
return isTrue ? content : ''
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Traite les blocs {{#unless}}...{{/unless}}
|
|
160
|
+
*/
|
|
161
|
+
processUnlessBlocks(template, data) {
|
|
162
|
+
const unlessPattern = /\{\{#unless\s+([^}]+)\}\}((?:(?!\{\{#unless)[\s\S])*?)\{\{\/unless\}\}/g;
|
|
163
|
+
|
|
164
|
+
return template.replace(unlessPattern, (match, condition, content) => {
|
|
165
|
+
const value = this.resolveNestedKey(data, condition.trim());
|
|
166
|
+
const isFalse = !Boolean(value);
|
|
167
|
+
|
|
168
|
+
return isFalse ? content : ''
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gestion basique des conditions {{#if}} et {{#unless}} (déprécié - utiliser processConditionalBlocks)
|
|
174
|
+
*/
|
|
175
|
+
handleConditional(data, key, isIf) {
|
|
176
|
+
const conditionKey = key.split(' ')[1];
|
|
177
|
+
const value = this.resolveNestedKey(data, conditionKey);
|
|
178
|
+
const condition = Boolean(value);
|
|
179
|
+
|
|
180
|
+
return (isIf ? condition : !condition) ? '' : '<!-- condition-false -->'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Templates de fallback en cas d'erreur de chargement
|
|
185
|
+
*/
|
|
186
|
+
getFallbackTemplate(templateName) {
|
|
187
|
+
const fallbacks = {
|
|
188
|
+
'CashbackTemplate': `
|
|
189
|
+
<div class="vipros-offer-card cashback-mode">
|
|
190
|
+
<div class="offer-content">
|
|
191
|
+
<div class="main-value">
|
|
192
|
+
<span class="amount">{{cashback.value}}</span>
|
|
193
|
+
<span class="currency">€</span>
|
|
194
|
+
<span class="label">remboursés</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
`,
|
|
199
|
+
'VIPointsTemplate': `
|
|
200
|
+
<div class="vipros-offer-card vipoints-mode">
|
|
201
|
+
<div class="offer-content">
|
|
202
|
+
<div class="main-value">
|
|
203
|
+
<span class="amount">{{vipoints.perTenEuros}}</span>
|
|
204
|
+
<span class="label">VIPoints</span>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`,
|
|
209
|
+
'LoadingTemplate': `
|
|
210
|
+
<div class="vipros-offer-card loading-state">
|
|
211
|
+
<div class="loading-text">Recherche...</div>
|
|
212
|
+
</div>
|
|
213
|
+
`,
|
|
214
|
+
'ErrorTemplate': `
|
|
215
|
+
<div class="vipros-offer-card error-state">
|
|
216
|
+
<div class="error-text">{{message}}</div>
|
|
217
|
+
<div class="debug-notice">Erreur affichée car le mode debug est activé</div>
|
|
218
|
+
</div>
|
|
219
|
+
`
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return fallbacks[templateName] || '<div class="vipros-offer-card">Template not found</div>'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Précharge tous les templates pour de meilleures performances
|
|
227
|
+
*/
|
|
228
|
+
async preloadTemplates() {
|
|
229
|
+
const templateNames = ['CashbackTemplate', 'VIPointsTemplate', 'LoadingTemplate', 'ErrorTemplate'];
|
|
230
|
+
|
|
231
|
+
const promises = templateNames.map(name =>
|
|
232
|
+
this.loadTemplate(name).catch(error => {
|
|
233
|
+
if (this.debug) console.warn(`[TemplateLoader] Failed to preload ${name}:`, error.message);
|
|
234
|
+
return null
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
await Promise.all(promises);
|
|
239
|
+
if (this.debug) console.log('[TemplateLoader] Templates preloaded');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Nettoie le cache des templates
|
|
244
|
+
*/
|
|
245
|
+
clearCache() {
|
|
246
|
+
this.templates.clear();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* ProductDataTransformer - Transformation des données API vers format composant
|
|
252
|
+
*
|
|
253
|
+
* Centralise toute la logique de transformation des données API
|
|
254
|
+
* vers le format attendu par les templates.
|
|
255
|
+
*/
|
|
256
|
+
class ProductDataTransformer {
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Transforme les données API vers le format template
|
|
260
|
+
*/
|
|
261
|
+
static transform(apiProduct, price = null) {
|
|
262
|
+
if (!apiProduct) {
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const hasCashback = Boolean(apiProduct.cashback?.value > 0);
|
|
267
|
+
const generosityRate = apiProduct.brand?.generosity_rate || 0;
|
|
268
|
+
|
|
269
|
+
// Calculer les VIPoints selon le contexte
|
|
270
|
+
const vipointsData = this.calculateVIPoints(generosityRate, price);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
id: apiProduct.id || 'not_found',
|
|
274
|
+
ean: apiProduct.ean,
|
|
275
|
+
name: apiProduct.name || 'Produit inconnu',
|
|
276
|
+
price: price,
|
|
277
|
+
|
|
278
|
+
brand: this.transformBrandData(apiProduct.brand),
|
|
279
|
+
cashback: this.transformCashbackData(apiProduct.cashback),
|
|
280
|
+
vipoints: vipointsData,
|
|
281
|
+
|
|
282
|
+
hasCashback,
|
|
283
|
+
viprosLink: this.buildViprosLink(apiProduct),
|
|
284
|
+
|
|
285
|
+
// URL de l'image VIPoints calculée dynamiquement
|
|
286
|
+
vipointsImageUrl: null // Sera définie par le composant
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Transforme les données de marque
|
|
292
|
+
*/
|
|
293
|
+
static transformBrandData(brandData) {
|
|
294
|
+
if (!brandData) {
|
|
295
|
+
return {
|
|
296
|
+
name: 'Marque inconnue',
|
|
297
|
+
displayName: null,
|
|
298
|
+
logoUrl: null,
|
|
299
|
+
generosityRate: 0
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const displayName = brandData.name && brandData.name !== 'Marque inconnue'
|
|
304
|
+
? brandData.name
|
|
305
|
+
: null;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
name: brandData.name || 'Marque inconnue',
|
|
309
|
+
displayName,
|
|
310
|
+
logoUrl: brandData.logo_url || null,
|
|
311
|
+
generosityRate: brandData.generosity_rate || 0
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Transforme les données de cashback
|
|
317
|
+
*/
|
|
318
|
+
static transformCashbackData(cashbackData) {
|
|
319
|
+
if (!cashbackData) {
|
|
320
|
+
return {
|
|
321
|
+
value: 0,
|
|
322
|
+
name: null,
|
|
323
|
+
imageUrl: null,
|
|
324
|
+
startDate: null,
|
|
325
|
+
endDate: null
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
value: cashbackData.value || 0,
|
|
331
|
+
name: cashbackData.name || null,
|
|
332
|
+
imageUrl: cashbackData.image_url || null,
|
|
333
|
+
startDate: cashbackData.start_date || null,
|
|
334
|
+
endDate: cashbackData.end_date || null
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Calcule les données VIPoints selon le contexte
|
|
340
|
+
*/
|
|
341
|
+
static calculateVIPoints(generosityRate, price) {
|
|
342
|
+
let perTenEuros = 0;
|
|
343
|
+
let hasBonus = false;
|
|
344
|
+
let showExact = false;
|
|
345
|
+
|
|
346
|
+
if (generosityRate > 0) {
|
|
347
|
+
hasBonus = true;
|
|
348
|
+
|
|
349
|
+
if (price && price > 0) {
|
|
350
|
+
// Prix renseigné : calculer le nombre exact de VIPoints
|
|
351
|
+
perTenEuros = Math.floor((generosityRate * price) / 10);
|
|
352
|
+
showExact = true;
|
|
353
|
+
} else {
|
|
354
|
+
// Pas de prix : affichage générique "1 VIPoint tous les 10€"
|
|
355
|
+
perTenEuros = 1;
|
|
356
|
+
showExact = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
perTenEuros,
|
|
362
|
+
generosityRate,
|
|
363
|
+
hasBonus,
|
|
364
|
+
showExact
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Détermine le mode d'affichage selon les données
|
|
370
|
+
*/
|
|
371
|
+
static determineDisplayMode(transformedData) {
|
|
372
|
+
if (!transformedData) {
|
|
373
|
+
return 'empty'
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (transformedData.hasCashback) {
|
|
377
|
+
return 'cashback'
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (transformedData.vipoints.hasBonus) {
|
|
381
|
+
return 'vipoints'
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return 'empty'
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Valide les données transformées
|
|
389
|
+
*/
|
|
390
|
+
static validate(transformedData) {
|
|
391
|
+
if (!transformedData) {
|
|
392
|
+
return { isValid: false, errors: ['No data provided'] }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const errors = [];
|
|
396
|
+
|
|
397
|
+
if (!transformedData.ean) {
|
|
398
|
+
errors.push('EAN is required');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (transformedData.hasCashback && !transformedData.cashback.value) {
|
|
402
|
+
errors.push('Cashback value is required when hasCashback is true');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (transformedData.vipoints.hasBonus && !transformedData.vipoints.generosityRate) {
|
|
406
|
+
errors.push('Generosity rate is required when VIPoints bonus is available');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
isValid: errors.length === 0,
|
|
411
|
+
errors
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Prépare les données pour le template
|
|
417
|
+
*/
|
|
418
|
+
static prepareTemplateData(transformedData, vipointsImageUrl = null) {
|
|
419
|
+
if (!transformedData) {
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
...transformedData,
|
|
425
|
+
vipointsImageUrl: vipointsImageUrl || this.getDefaultVIPointsImageUrl()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Construit l'URL VIPros complète avec les paramètres origin
|
|
431
|
+
*/
|
|
432
|
+
static buildViprosLink(apiProduct) {
|
|
433
|
+
const baseUrl = apiProduct.vipros_link || 'https://vipros.fr';
|
|
434
|
+
|
|
435
|
+
// Récupérer le nom du point de vente depuis les données API
|
|
436
|
+
const origin = this.getSalesPointName(apiProduct);
|
|
437
|
+
|
|
438
|
+
// URL de la page actuelle
|
|
439
|
+
const originUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
440
|
+
|
|
441
|
+
// Construire l'URL avec les paramètres
|
|
442
|
+
const url = new URL(baseUrl);
|
|
443
|
+
if (origin) {
|
|
444
|
+
url.searchParams.set('origin', origin);
|
|
445
|
+
}
|
|
446
|
+
if (originUrl) {
|
|
447
|
+
url.searchParams.set('origin_url', originUrl);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return url.toString()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Extrait le nom du point de vente depuis les données API
|
|
455
|
+
*/
|
|
456
|
+
static getSalesPointName(apiProduct) {
|
|
457
|
+
// Chercher dans les sales_points nested
|
|
458
|
+
if (apiProduct.sales_points && Array.isArray(apiProduct.sales_points)) {
|
|
459
|
+
const salesPoint = apiProduct.sales_points.find(sp => sp.name);
|
|
460
|
+
if (salesPoint) {
|
|
461
|
+
return salesPoint.name
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Fallback : utiliser le domaine actuel comme nom du point de vente
|
|
466
|
+
if (typeof window !== 'undefined') {
|
|
467
|
+
const hostname = window.location.hostname;
|
|
468
|
+
// Transformer le domaine en nom plus lisible
|
|
469
|
+
return hostname.replace(/^www\./, '').replace(/\./g, ' ').split(' ')[0]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return null
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* URL par défaut de l'image VIPoints
|
|
477
|
+
*/
|
|
478
|
+
static getDefaultVIPointsImageUrl() {
|
|
479
|
+
// Sera définie dynamiquement par le composant selon son contexte
|
|
480
|
+
return '/api/sdk/assets/image-vipoints.jpg'
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* DisplayModeResolver - Résolution du mode d'affichage
|
|
486
|
+
*
|
|
487
|
+
* Détermine comment afficher le composant selon les données produit
|
|
488
|
+
* et les options de configuration utilisateur.
|
|
489
|
+
*/
|
|
490
|
+
class DisplayModeResolver {
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Résout le mode d'affichage principal
|
|
494
|
+
*/
|
|
495
|
+
static resolveDisplayMode(productData) {
|
|
496
|
+
if (!productData) {
|
|
497
|
+
return 'empty'
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Mode cashback prioritaire
|
|
501
|
+
if (productData.hasCashback) {
|
|
502
|
+
return 'cashback'
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Mode VIPoints si disponible
|
|
506
|
+
if (productData.vipoints.hasBonus) {
|
|
507
|
+
return 'vipoints'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Aucune offre disponible
|
|
511
|
+
return 'empty'
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Résout le template à utiliser selon le mode
|
|
516
|
+
*/
|
|
517
|
+
static resolveTemplateName(displayMode) {
|
|
518
|
+
const templateMap = {
|
|
519
|
+
'cashback': 'CashbackTemplate',
|
|
520
|
+
'vipoints': 'VIPointsTemplate',
|
|
521
|
+
'loading': 'LoadingTemplate',
|
|
522
|
+
'error': 'ErrorTemplate',
|
|
523
|
+
'empty': null,
|
|
524
|
+
'hidden': null
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
return templateMap[displayMode] || null
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Détermine si le composant doit être visible
|
|
532
|
+
*/
|
|
533
|
+
static shouldDisplay(displayMode) {
|
|
534
|
+
return !['empty', 'hidden'].includes(displayMode)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Résout la classe CSS du conteneur
|
|
539
|
+
*/
|
|
540
|
+
static resolveContainerClass(displayMode) {
|
|
541
|
+
const classMap = {
|
|
542
|
+
'cashback': 'cashback-mode',
|
|
543
|
+
'vipoints': 'vipoints-mode',
|
|
544
|
+
'loading': 'loading-state',
|
|
545
|
+
'error': 'error-state',
|
|
546
|
+
'empty': 'empty-state',
|
|
547
|
+
'hidden': 'hidden-state'
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
return `vipros-offer-card ${classMap[displayMode] || ''}`
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Validation du mode d'affichage
|
|
555
|
+
*/
|
|
556
|
+
static validateDisplayMode(displayMode) {
|
|
557
|
+
const validModes = ['cashback', 'vipoints', 'loading', 'error', 'empty', 'hidden'];
|
|
558
|
+
|
|
559
|
+
if (!validModes.includes(displayMode)) {
|
|
560
|
+
console.warn(`[DisplayModeResolver] Invalid display mode: ${displayMode}`);
|
|
561
|
+
return 'error'
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return displayMode
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Analyse complète pour déterminer l'affichage
|
|
569
|
+
*/
|
|
570
|
+
static analyze(productData, options = {}) {
|
|
571
|
+
const {
|
|
572
|
+
isLoading = false,
|
|
573
|
+
error = null,
|
|
574
|
+
debug = false
|
|
575
|
+
} = options;
|
|
576
|
+
|
|
577
|
+
// États d'erreur et de chargement prioritaires
|
|
578
|
+
if (error) {
|
|
579
|
+
// Afficher les erreurs seulement si debug est activé
|
|
580
|
+
if (debug) {
|
|
581
|
+
return {
|
|
582
|
+
mode: 'error',
|
|
583
|
+
template: 'ErrorTemplate',
|
|
584
|
+
shouldDisplay: true,
|
|
585
|
+
containerClass: this.resolveContainerClass('error'),
|
|
586
|
+
data: {
|
|
587
|
+
message: error.message || 'Une erreur est survenue',
|
|
588
|
+
debugMode: true
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
// En mode non-debug, ne pas afficher les erreurs (masquer complètement)
|
|
593
|
+
return {
|
|
594
|
+
mode: 'hidden',
|
|
595
|
+
template: null,
|
|
596
|
+
shouldDisplay: false,
|
|
597
|
+
containerClass: this.resolveContainerClass('hidden'),
|
|
598
|
+
data: {}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (isLoading) {
|
|
604
|
+
return {
|
|
605
|
+
mode: 'loading',
|
|
606
|
+
template: 'LoadingTemplate',
|
|
607
|
+
shouldDisplay: true,
|
|
608
|
+
containerClass: this.resolveContainerClass('loading'),
|
|
609
|
+
data: {}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Résolution normale
|
|
614
|
+
const mode = this.resolveDisplayMode(productData);
|
|
615
|
+
const template = this.resolveTemplateName(mode);
|
|
616
|
+
const shouldDisplay = this.shouldDisplay(mode);
|
|
617
|
+
const containerClass = this.resolveContainerClass(mode);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
mode: this.validateDisplayMode(mode),
|
|
621
|
+
template,
|
|
622
|
+
shouldDisplay,
|
|
623
|
+
containerClass,
|
|
624
|
+
data: shouldDisplay ? productData : {}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Vérifie si les données sont suffisantes pour l'affichage
|
|
630
|
+
*/
|
|
631
|
+
static hasMinimumRequiredData(productData) {
|
|
632
|
+
if (!productData) {
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Au minimum besoin d'un EAN
|
|
637
|
+
if (!productData.ean) {
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Doit avoir soit cashback soit VIPoints
|
|
642
|
+
return productData.hasCashback || productData.vipoints.hasBonus
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Suggestions d'amélioration des données
|
|
647
|
+
*/
|
|
648
|
+
static suggestDataImprovements(productData) {
|
|
649
|
+
if (!productData) {
|
|
650
|
+
return ['Product data is required']
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const suggestions = [];
|
|
654
|
+
|
|
655
|
+
if (!productData.name || productData.name === 'Produit inconnu') {
|
|
656
|
+
suggestions.push('Product name could be improved');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (productData.hasCashback && !productData.cashback.imageUrl) {
|
|
660
|
+
suggestions.push('Cashback image would improve visual appeal');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (!productData.price && productData.vipoints.hasBonus) {
|
|
664
|
+
suggestions.push('Price would enable exact VIPoints calculation');
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (!productData.brand.displayName) {
|
|
668
|
+
suggestions.push('Brand name would improve VIPoints messaging');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return suggestions
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* EventManager - Gestionnaire d'événements pour ViprosOfferCard
|
|
677
|
+
*
|
|
678
|
+
* Centralise la gestion des événements du composant Web Component.
|
|
679
|
+
* Simplifie l'émission et l'écoute d'événements personnalisés.
|
|
680
|
+
*/
|
|
681
|
+
class EventManager {
|
|
682
|
+
constructor(element) {
|
|
683
|
+
this.element = element;
|
|
684
|
+
this.listeners = new Map();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Émet un événement personnalisé
|
|
689
|
+
*/
|
|
690
|
+
emit(eventName, detail = {}) {
|
|
691
|
+
const event = new CustomEvent(eventName, {
|
|
692
|
+
detail,
|
|
693
|
+
bubbles: true,
|
|
694
|
+
cancelable: true
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
this.element.dispatchEvent(event);
|
|
698
|
+
|
|
699
|
+
// Log pour debugging
|
|
700
|
+
if (this.element.getAttribute('debug') === 'true') {
|
|
701
|
+
console.log(`[ViprosOfferCard] Event emitted: ${eventName}`, detail);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Émet l'événement d'offre chargée
|
|
707
|
+
*/
|
|
708
|
+
emitOfferLoaded(productData, ean) {
|
|
709
|
+
this.emit('vipros-offer-loaded', {
|
|
710
|
+
product: productData,
|
|
711
|
+
ean: ean,
|
|
712
|
+
timestamp: Date.now()
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Émet l'événement d'erreur
|
|
718
|
+
*/
|
|
719
|
+
emitError(error, ean) {
|
|
720
|
+
this.emit('vipros-offer-error', {
|
|
721
|
+
error: {
|
|
722
|
+
message: error.message || 'Unknown error',
|
|
723
|
+
type: error.constructor.name,
|
|
724
|
+
stack: error.stack
|
|
725
|
+
},
|
|
726
|
+
ean: ean,
|
|
727
|
+
timestamp: Date.now()
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Émet l'événement de clic sur offre
|
|
733
|
+
*/
|
|
734
|
+
emitOfferClick(url, productData, ean) {
|
|
735
|
+
this.emit('vipros-offer-click', {
|
|
736
|
+
url,
|
|
737
|
+
product: productData,
|
|
738
|
+
ean: ean,
|
|
739
|
+
timestamp: Date.now()
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Émet l'événement de synchronisation démarrée
|
|
745
|
+
*/
|
|
746
|
+
emitSyncStarted(ean, url) {
|
|
747
|
+
this.emit('vipros-sync-started', {
|
|
748
|
+
ean,
|
|
749
|
+
url,
|
|
750
|
+
timestamp: Date.now()
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Émet l'événement de synchronisation réussie
|
|
756
|
+
*/
|
|
757
|
+
emitSyncSuccess(ean, result) {
|
|
758
|
+
this.emit('vipros-sync-success', {
|
|
759
|
+
ean,
|
|
760
|
+
result,
|
|
761
|
+
timestamp: Date.now()
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Émet l'événement d'erreur de synchronisation
|
|
767
|
+
*/
|
|
768
|
+
emitSyncError(ean, error) {
|
|
769
|
+
this.emit('vipros-sync-error', {
|
|
770
|
+
ean,
|
|
771
|
+
error: {
|
|
772
|
+
message: error.message || 'Sync error',
|
|
773
|
+
type: error.constructor.name
|
|
774
|
+
},
|
|
775
|
+
timestamp: Date.now()
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Ajoute des listeners pour les clics sur les liens
|
|
781
|
+
*/
|
|
782
|
+
attachLinkListeners(shadowRoot, productData, ean) {
|
|
783
|
+
const links = shadowRoot.querySelectorAll('a[href]');
|
|
784
|
+
|
|
785
|
+
links.forEach(link => {
|
|
786
|
+
const clickHandler = (event) => {
|
|
787
|
+
this.emitOfferClick(link.href, productData, ean);
|
|
788
|
+
|
|
789
|
+
// Optionnel : tracking analytics
|
|
790
|
+
if (window.gtag) {
|
|
791
|
+
window.gtag('event', 'vipros_offer_click', {
|
|
792
|
+
ean: ean,
|
|
793
|
+
url: link.href,
|
|
794
|
+
product_name: productData?.name
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
link.addEventListener('click', clickHandler);
|
|
800
|
+
|
|
801
|
+
// Stocker la référence pour cleanup
|
|
802
|
+
if (!this.listeners.has('linkClicks')) {
|
|
803
|
+
this.listeners.set('linkClicks', []);
|
|
804
|
+
}
|
|
805
|
+
this.listeners.get('linkClicks').push({ link, handler: clickHandler });
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Supprime tous les listeners attachés
|
|
811
|
+
*/
|
|
812
|
+
cleanup() {
|
|
813
|
+
// Nettoyer les listeners de liens
|
|
814
|
+
if (this.listeners.has('linkClicks')) {
|
|
815
|
+
this.listeners.get('linkClicks').forEach(({ link, handler }) => {
|
|
816
|
+
link.removeEventListener('click', handler);
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
this.listeners.clear();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Configuration des événements globaux pour debugging
|
|
825
|
+
*/
|
|
826
|
+
enableGlobalDebugging() {
|
|
827
|
+
const events = [
|
|
828
|
+
'vipros-offer-loaded',
|
|
829
|
+
'vipros-offer-error',
|
|
830
|
+
'vipros-offer-click',
|
|
831
|
+
'vipros-sync-started',
|
|
832
|
+
'vipros-sync-success',
|
|
833
|
+
'vipros-sync-error'
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
events.forEach(eventName => {
|
|
837
|
+
document.addEventListener(eventName, (event) => {
|
|
838
|
+
console.log(`[ViprosOfferCard] Global event: ${eventName}`, event.detail);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Méthodes statiques pour les composants sans instance
|
|
845
|
+
*/
|
|
846
|
+
static emit(element, eventName, detail = {}) {
|
|
847
|
+
const manager = new EventManager(element);
|
|
848
|
+
manager.emit(eventName, detail);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
static emitOfferLoaded(element, productData, ean) {
|
|
852
|
+
const manager = new EventManager(element);
|
|
853
|
+
manager.emitOfferLoaded(productData, ean);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
static emitError(element, error, ean) {
|
|
857
|
+
const manager = new EventManager(element);
|
|
858
|
+
manager.emitError(error, ean);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* ViprosOfferCard - Web Component for displaying VIPros offers
|
|
864
|
+
*/
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
class ViprosOfferCard extends HTMLElement {
|
|
868
|
+
constructor() {
|
|
869
|
+
super();
|
|
870
|
+
|
|
871
|
+
// Shadow DOM for component isolation
|
|
872
|
+
this.attachShadow({ mode: 'open' });
|
|
873
|
+
|
|
874
|
+
this.templateLoader = new TemplateLoader();
|
|
875
|
+
this.eventManager = new EventManager(this);
|
|
876
|
+
|
|
877
|
+
// Component state
|
|
878
|
+
this.state = {
|
|
879
|
+
isLoading: false,
|
|
880
|
+
productData: null,
|
|
881
|
+
error: null,
|
|
882
|
+
apiClient: null
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// Bind methods
|
|
886
|
+
this.init = this.init.bind(this);
|
|
887
|
+
this.loadProduct = this.loadProduct.bind(this);
|
|
888
|
+
this.render = this.render.bind(this);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Observed attributes
|
|
892
|
+
static get observedAttributes() {
|
|
893
|
+
return [
|
|
894
|
+
'ean', 'price', 'api-base-url', 'api-key',
|
|
895
|
+
'primary-color', 'text-color', 'background-color',
|
|
896
|
+
'card-bonus-bg', 'card-bonus-color'
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Attribute getters
|
|
901
|
+
get ean() {
|
|
902
|
+
return this.getAttribute('ean') || this.getConfigValue('ean')
|
|
903
|
+
}
|
|
904
|
+
get price() {
|
|
905
|
+
const attrPrice = parseFloat(this.getAttribute('price')) || 0;
|
|
906
|
+
if (attrPrice > 0) return attrPrice
|
|
907
|
+
|
|
908
|
+
const configPrice = this.getConfigValue('price');
|
|
909
|
+
if (configPrice && parseFloat(configPrice) > 0) return parseFloat(configPrice)
|
|
910
|
+
|
|
911
|
+
return null
|
|
912
|
+
}
|
|
913
|
+
get apiBaseUrl() {
|
|
914
|
+
return this.getConfigValue('apiBaseUrl') || this.getAttribute('api-base-url') || this.detectFallbackApiBaseUrl()
|
|
915
|
+
}
|
|
916
|
+
get apiKey() {
|
|
917
|
+
return this.getConfigValue('apiKey') || this.getAttribute('api-key')
|
|
918
|
+
}
|
|
919
|
+
get primaryColor() {
|
|
920
|
+
return this.getAttribute('primary-color') || this.getConfigValue('primaryColor')
|
|
921
|
+
}
|
|
922
|
+
get textColor() {
|
|
923
|
+
return this.getAttribute('text-color') || this.getConfigValue('textColor')
|
|
924
|
+
}
|
|
925
|
+
get backgroundColor() {
|
|
926
|
+
return this.getAttribute('background-color') || this.getConfigValue('backgroundColor')
|
|
927
|
+
}
|
|
928
|
+
get cardBonusBg() {
|
|
929
|
+
return this.getAttribute('card-bonus-bg') || this.getConfigValue('cardBonusBg')
|
|
930
|
+
}
|
|
931
|
+
get cardBonusColor() {
|
|
932
|
+
return this.getAttribute('card-bonus-color') || this.getConfigValue('cardBonusColor')
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// SDK configuration helpers
|
|
936
|
+
getConfigValue(key) {
|
|
937
|
+
const sdk = this.getSDKInstance();
|
|
938
|
+
return sdk?.config?.[key] || null
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
getSDKInstance() {
|
|
942
|
+
return typeof window !== 'undefined' ? window.ViprosSDK : null
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
detectFallbackApiBaseUrl() {
|
|
946
|
+
// Edge case: No window object (SSR or Node.js environment)
|
|
947
|
+
if (typeof window === 'undefined') {
|
|
948
|
+
this.log('[ViprosOfferCard] No window object - defaulting to production API');
|
|
949
|
+
return 'https://msys.vipros.fr/api'
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
// Edge case: window.location is not available or corrupted
|
|
954
|
+
if (!window.location || typeof window.location.hostname !== 'string') {
|
|
955
|
+
this.log('[ViprosOfferCard] Invalid window.location - defaulting to production API');
|
|
956
|
+
return 'https://msys.vipros.fr/api'
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const protocol = window.location.protocol || 'https:';
|
|
960
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
961
|
+
|
|
962
|
+
// Edge case: Empty or invalid hostname
|
|
963
|
+
if (!hostname || hostname.length === 0) {
|
|
964
|
+
this.log('[ViprosOfferCard] Empty hostname - defaulting to production API');
|
|
965
|
+
return 'https://msys.vipros.fr/api'
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Edge case: Local development or non-standard protocols
|
|
969
|
+
if (protocol === 'file:' || hostname === 'localhost' || hostname.startsWith('127.0.0.1') || hostname.startsWith('0.0.0.0')) {
|
|
970
|
+
this.log('[ViprosOfferCard] Local development detected - using DDEV fallback');
|
|
971
|
+
return 'https://vipros-connect.ddev.site/api'
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Correspondance directe des domaines (même logique que ConfigManager)
|
|
975
|
+
if (hostname === 'vipros-connect.ddev.site') {
|
|
976
|
+
return `${protocol}//${hostname}/api`
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (hostname === 'msys.preprod.vipros.fr') {
|
|
980
|
+
return `${protocol}//msys.preprod.vipros.fr/api`
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Edge case: Subdomain detection for enterprise customers
|
|
984
|
+
if (hostname.endsWith('.vipros.fr')) {
|
|
985
|
+
this.log(`[ViprosOfferCard] VIPros subdomain detected: ${hostname}`);
|
|
986
|
+
return `${protocol}//${hostname}/api`
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Par défaut : production (toujours HTTPS pour la sécurité)
|
|
990
|
+
this.log(`[ViprosOfferCard] Unknown domain: ${hostname} - defaulting to production API`);
|
|
991
|
+
return 'https://msys.vipros.fr/api'
|
|
992
|
+
|
|
993
|
+
} catch (error) {
|
|
994
|
+
// Edge case: Exception during detection
|
|
995
|
+
this.log(`[ViprosOfferCard] Error detecting API base URL: ${error.message} - defaulting to production`);
|
|
996
|
+
return 'https://msys.vipros.fr/api'
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Lifecycle methods
|
|
1001
|
+
connectedCallback() {
|
|
1002
|
+
this.log('[ViprosOfferCard] Component connected');
|
|
1003
|
+
this.init();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
1007
|
+
if (oldValue !== newValue && this.isConnected) {
|
|
1008
|
+
if (name === 'ean') {
|
|
1009
|
+
this.log(`[ViprosOfferCard] EAN changed: ${oldValue} → ${newValue}`);
|
|
1010
|
+
this.loadProduct();
|
|
1011
|
+
} else if ([
|
|
1012
|
+
'primary-color', 'text-color', 'background-color',
|
|
1013
|
+
'gift-icon-color', 'gift-card-bonus-bg', 'gift-card-bonus-color',
|
|
1014
|
+
'vipoints-bonus-bg', 'vipoints-bonus-color'
|
|
1015
|
+
].includes(name)) {
|
|
1016
|
+
this.log(`[ViprosOfferCard] Color changed: ${name} = ${newValue}`);
|
|
1017
|
+
this.updateCustomColors();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
disconnectedCallback() {
|
|
1023
|
+
this.log('[ViprosOfferCard] Component disconnected');
|
|
1024
|
+
this.cleanup();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Initialization
|
|
1028
|
+
async init() {
|
|
1029
|
+
try {
|
|
1030
|
+
// Load Google Fonts in document head
|
|
1031
|
+
this.loadGoogleFonts();
|
|
1032
|
+
|
|
1033
|
+
// Load CSS styles
|
|
1034
|
+
await this.loadStyles();
|
|
1035
|
+
|
|
1036
|
+
// Apply custom colors if provided
|
|
1037
|
+
this.updateCustomColors();
|
|
1038
|
+
|
|
1039
|
+
// Initialize API client
|
|
1040
|
+
await this.initApiClient();
|
|
1041
|
+
|
|
1042
|
+
// Preload templates
|
|
1043
|
+
await this.templateLoader.preloadTemplates();
|
|
1044
|
+
|
|
1045
|
+
// Load product data
|
|
1046
|
+
if (this.ean) {
|
|
1047
|
+
await this.loadProduct();
|
|
1048
|
+
} else {
|
|
1049
|
+
this.setError(new Error('EAN requis pour afficher une offre VIPros'));
|
|
1050
|
+
}
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
this.logError('[ViprosOfferCard] Init error:', error);
|
|
1053
|
+
this.setError(error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Load Google Fonts in document head
|
|
1058
|
+
loadGoogleFonts() {
|
|
1059
|
+
const fontsToLoad = [
|
|
1060
|
+
'https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap',
|
|
1061
|
+
'https://fonts.googleapis.com/css2?family=Encode+Sans+Condensed:wght@100;200;300;400;500;600;700;800;900&display=swap'
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
fontsToLoad.forEach(fontUrl => {
|
|
1065
|
+
// Check if font link already exists
|
|
1066
|
+
const existingLink = document.querySelector(`link[href="${fontUrl}"]`);
|
|
1067
|
+
if (!existingLink) {
|
|
1068
|
+
const linkElement = document.createElement('link');
|
|
1069
|
+
linkElement.rel = 'stylesheet';
|
|
1070
|
+
linkElement.href = fontUrl;
|
|
1071
|
+
linkElement.crossOrigin = 'anonymous';
|
|
1072
|
+
document.head.appendChild(linkElement);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Load CSS styles
|
|
1078
|
+
async loadStyles() {
|
|
1079
|
+
try {
|
|
1080
|
+
const styleUrl = this.templateLoader.baseUrl.replace('/components/templates', '/components/styles/offer-card.css');
|
|
1081
|
+
const response = await fetch(styleUrl);
|
|
1082
|
+
|
|
1083
|
+
if (response.ok) {
|
|
1084
|
+
const css = await response.text();
|
|
1085
|
+
const styleElement = document.createElement('style');
|
|
1086
|
+
styleElement.textContent = css;
|
|
1087
|
+
this.shadowRoot.appendChild(styleElement);
|
|
1088
|
+
} else {
|
|
1089
|
+
// Fallback avec styles minimaux
|
|
1090
|
+
this.injectFallbackStyles();
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
this.logError('[ViprosOfferCard] CSS loading failed, using fallback');
|
|
1094
|
+
this.injectFallbackStyles();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Update custom colors via CSS variables
|
|
1099
|
+
updateCustomColors() {
|
|
1100
|
+
if (!this.shadowRoot) return
|
|
1101
|
+
|
|
1102
|
+
const host = this.shadowRoot.host;
|
|
1103
|
+
const customColors = {};
|
|
1104
|
+
|
|
1105
|
+
// Map attributes to CSS variables
|
|
1106
|
+
if (this.primaryColor) {
|
|
1107
|
+
customColors['--vipros-primary'] = this.primaryColor;
|
|
1108
|
+
customColors['--vipros-text-accent'] = this.primaryColor;
|
|
1109
|
+
}
|
|
1110
|
+
if (this.textColor) {
|
|
1111
|
+
customColors['--vipros-text-main'] = this.textColor;
|
|
1112
|
+
}
|
|
1113
|
+
if (this.backgroundColor) {
|
|
1114
|
+
customColors['--vipros-background'] = this.backgroundColor;
|
|
1115
|
+
}
|
|
1116
|
+
if (this.cardBonusBg) {
|
|
1117
|
+
customColors['--card-bonus-bg'] = this.cardBonusBg;
|
|
1118
|
+
}
|
|
1119
|
+
if (this.cardBonusColor) {
|
|
1120
|
+
customColors['--card-bonus-color'] = this.cardBonusColor;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Apply custom colors to the host element
|
|
1124
|
+
Object.entries(customColors).forEach(([property, value]) => {
|
|
1125
|
+
host.style.setProperty(property, value);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
this.log('[ViprosOfferCard] Custom colors applied:', customColors);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
injectFallbackStyles() {
|
|
1132
|
+
const fallbackCSS = `
|
|
1133
|
+
.vipros-offer-card {
|
|
1134
|
+
display: block;
|
|
1135
|
+
padding: 20px;
|
|
1136
|
+
background: #e8ecf8;
|
|
1137
|
+
border-radius: 12px;
|
|
1138
|
+
font-family: sans-serif;
|
|
1139
|
+
}
|
|
1140
|
+
.loading-content, .error-content {
|
|
1141
|
+
text-align: center;
|
|
1142
|
+
padding: 40px;
|
|
1143
|
+
}
|
|
1144
|
+
`;
|
|
1145
|
+
const styleElement = document.createElement('style');
|
|
1146
|
+
styleElement.textContent = fallbackCSS;
|
|
1147
|
+
this.shadowRoot.appendChild(styleElement);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Méthodes de logging respectant le mode debug
|
|
1151
|
+
log(message, ...args) {
|
|
1152
|
+
const sdk = this.getSDKInstance();
|
|
1153
|
+
if (sdk && sdk.config && sdk.config.debug === true) {
|
|
1154
|
+
console.log(message, ...args);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
logError(message, ...args) {
|
|
1159
|
+
const sdk = this.getSDKInstance();
|
|
1160
|
+
if (sdk && sdk.config && sdk.config.debug === true) {
|
|
1161
|
+
console.error(message, ...args);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Initialisation API client
|
|
1166
|
+
async initApiClient() {
|
|
1167
|
+
const sdk = this.getSDKInstance();
|
|
1168
|
+
|
|
1169
|
+
if (sdk && sdk.getApiClient) {
|
|
1170
|
+
this.state.apiClient = sdk.getApiClient();
|
|
1171
|
+
this.log('[ViprosOfferCard] Using shared SDK ApiClient');
|
|
1172
|
+
} else {
|
|
1173
|
+
// Fallback : créer son propre ApiClient
|
|
1174
|
+
const { default: ApiClient } = await Promise.resolve().then(function () { return ApiClient$1; });
|
|
1175
|
+
this.state.apiClient = new ApiClient({
|
|
1176
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
1177
|
+
apiKey: this.apiKey
|
|
1178
|
+
});
|
|
1179
|
+
this.log('[ViprosOfferCard] Created local ApiClient');
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Chargement des données produit
|
|
1184
|
+
async loadProduct() {
|
|
1185
|
+
if (!this.ean || this.state.isLoading) return
|
|
1186
|
+
|
|
1187
|
+
// Attendre que l'apiClient soit prêt
|
|
1188
|
+
if (!this.state.apiClient) {
|
|
1189
|
+
setTimeout(() => this.loadProduct(), 100);
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
this.setLoading(true);
|
|
1194
|
+
|
|
1195
|
+
try {
|
|
1196
|
+
this.log(`[ViprosOfferCard] Loading product: ${this.ean}`);
|
|
1197
|
+
|
|
1198
|
+
// NOUVEAU Phase 8: Préparer les options pour la sync inline si activée
|
|
1199
|
+
const sdk = this.getSDKInstance();
|
|
1200
|
+
const apiOptions = {
|
|
1201
|
+
timeout: 5000 // 5 secondes pour éviter les timeouts longs sur rate limit
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// Activer la sync inline si configuré dans le SDK
|
|
1205
|
+
if (sdk?.config?.inlineSyncEnabled) {
|
|
1206
|
+
apiOptions.enableInlineSync = true;
|
|
1207
|
+
apiOptions.productUrl = window.location.href;
|
|
1208
|
+
this.log(`[ViprosOfferCard] Inline sync enabled for EAN ${this.ean}`);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Appel API avec options de sync inline
|
|
1212
|
+
const response = await this.state.apiClient.get('/catalog/items', {
|
|
1213
|
+
ean: this.ean,
|
|
1214
|
+
items_per_page: 1
|
|
1215
|
+
}, apiOptions);
|
|
1216
|
+
|
|
1217
|
+
const bloomMetadata = response?.metadata?.bloomFilter;
|
|
1218
|
+
const debugEnabled = Boolean(this.getConfigValue('debug') || this.getAttribute('debug') === 'true');
|
|
1219
|
+
|
|
1220
|
+
if (bloomMetadata?.skipped) {
|
|
1221
|
+
this.log(`[ViprosOfferCard] Bloom filter skipped API call for EAN ${this.ean}`);
|
|
1222
|
+
|
|
1223
|
+
if (debugEnabled) {
|
|
1224
|
+
const bloomError = new Error('404 | Produit introuvable (Bloom filter)');
|
|
1225
|
+
bloomError.status = 404;
|
|
1226
|
+
bloomError.type = 'BLOOM_FILTER_NOT_FOUND';
|
|
1227
|
+
bloomError.data = {
|
|
1228
|
+
ean: this.ean,
|
|
1229
|
+
reason: bloomMetadata.reason || 'NOT_IN_FILTER'
|
|
1230
|
+
};
|
|
1231
|
+
this.setError(bloomError);
|
|
1232
|
+
} else {
|
|
1233
|
+
this.setProductData(null);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Log sync metadata if present (Phase 8)
|
|
1240
|
+
const syncMetadata = response?.sdk_metadata?.sync;
|
|
1241
|
+
if (syncMetadata && this.getConfigValue('debug')) {
|
|
1242
|
+
this.log(`[ViprosOfferCard] Sync metadata:`, syncMetadata);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (response.items && response.items.length > 0) {
|
|
1246
|
+
// Transformer les données
|
|
1247
|
+
const productData = ProductDataTransformer.transform(response.items[0], this.price);
|
|
1248
|
+
productData.vipointsImageUrl = this.getVIPointsImageUrl();
|
|
1249
|
+
|
|
1250
|
+
this.setProductData(productData);
|
|
1251
|
+
this.log(`[ViprosOfferCard] Product loaded: ${productData.name}`);
|
|
1252
|
+
|
|
1253
|
+
// Synchronisation en arrière-plan (seulement si inline sync n'a pas été tentée)
|
|
1254
|
+
// On vérifie si sync metadata existe, peu importe si triggered=true ou cooldown
|
|
1255
|
+
const inlineSyncWasAttempted = syncMetadata !== undefined && syncMetadata !== null;
|
|
1256
|
+
if (!inlineSyncWasAttempted) {
|
|
1257
|
+
this.triggerBackgroundSync();
|
|
1258
|
+
} else {
|
|
1259
|
+
this.log(`[ViprosOfferCard] Skipping background sync (inline sync was attempted, status: ${syncMetadata.reason || 'unknown'})`);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Émettre événement
|
|
1263
|
+
this.eventManager.emitOfferLoaded(productData, this.ean);
|
|
1264
|
+
} else {
|
|
1265
|
+
this.log(`[ViprosOfferCard] No product found for EAN: ${this.ean}`);
|
|
1266
|
+
this.setProductData(null);
|
|
1267
|
+
}
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
this.logError(`[ViprosOfferCard] Error loading EAN ${this.ean}:`, error);
|
|
1270
|
+
|
|
1271
|
+
// Gestion spécifique des erreurs rate limit
|
|
1272
|
+
if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
|
|
1273
|
+
const rateLimitError = new Error('API rate limit exceeded - Trop de requêtes simultanées');
|
|
1274
|
+
rateLimitError.status = 429;
|
|
1275
|
+
rateLimitError.type = 'RATE_LIMIT_ERROR';
|
|
1276
|
+
this.setError(rateLimitError);
|
|
1277
|
+
} else {
|
|
1278
|
+
this.setError(error);
|
|
1279
|
+
}
|
|
1280
|
+
} finally {
|
|
1281
|
+
this.setLoading(false);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// Gestion d'état
|
|
1286
|
+
setLoading(isLoading) {
|
|
1287
|
+
this.state.isLoading = isLoading;
|
|
1288
|
+
// Un seul render() - il gère lui-même l'état loading
|
|
1289
|
+
this.render();
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
setProductData(productData) {
|
|
1293
|
+
this.state.productData = productData;
|
|
1294
|
+
this.state.error = null;
|
|
1295
|
+
// Ne pas render ici, sera fait par setLoading(false)
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
setError(error) {
|
|
1299
|
+
this.state.error = error;
|
|
1300
|
+
this.state.productData = null;
|
|
1301
|
+
this.render();
|
|
1302
|
+
this.eventManager.emitError(error, this.ean);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Rendu principal
|
|
1306
|
+
async render() {
|
|
1307
|
+
// Toujours analyser l'état actuel pour déterminer le template
|
|
1308
|
+
const analysis = DisplayModeResolver.analyze(this.state.productData, {
|
|
1309
|
+
isLoading: this.state.isLoading,
|
|
1310
|
+
error: this.state.error,
|
|
1311
|
+
debug: this.getConfigValue('debug') || false
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
if (!analysis.shouldDisplay) {
|
|
1315
|
+
this.updateShadowContent('<div class="vipros-offer-card hidden"></div>');
|
|
1316
|
+
return
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (analysis.template) {
|
|
1320
|
+
const html = await this.templateLoader.render(analysis.template, analysis.data);
|
|
1321
|
+
|
|
1322
|
+
// Mettre à jour le contenu en préservant les styles
|
|
1323
|
+
this.updateShadowContent(html);
|
|
1324
|
+
|
|
1325
|
+
// Attacher les event listeners seulement si pas en loading
|
|
1326
|
+
if (!this.state.isLoading) {
|
|
1327
|
+
this.attachEventListeners();
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Rendus spécialisés (déprécié - utiliser render() directement)
|
|
1333
|
+
async renderLoading() {
|
|
1334
|
+
// Cette méthode est dépréciée - render() gère maintenant le loading
|
|
1335
|
+
await this.render();
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Utilitaire pour mettre à jour le contenu en préservant les styles
|
|
1339
|
+
updateShadowContent(html) {
|
|
1340
|
+
// Collecter tous les styles existants
|
|
1341
|
+
const styles = Array.from(this.shadowRoot.querySelectorAll('style'));
|
|
1342
|
+
const styleTexts = styles.map(style => style.textContent);
|
|
1343
|
+
|
|
1344
|
+
// Destruction complète du contenu
|
|
1345
|
+
this.shadowRoot.replaceChildren();
|
|
1346
|
+
|
|
1347
|
+
// Parser le nouveau HTML et l'ajouter
|
|
1348
|
+
const tempDiv = document.createElement('div');
|
|
1349
|
+
tempDiv.innerHTML = html;
|
|
1350
|
+
|
|
1351
|
+
// Transférer tous les enfants du div temporaire vers shadowRoot
|
|
1352
|
+
while (tempDiv.firstChild) {
|
|
1353
|
+
this.shadowRoot.appendChild(tempDiv.firstChild);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Recréer les styles
|
|
1357
|
+
styleTexts.forEach(styleText => {
|
|
1358
|
+
const styleElement = document.createElement('style');
|
|
1359
|
+
styleElement.textContent = styleText;
|
|
1360
|
+
this.shadowRoot.appendChild(styleElement);
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Event listeners
|
|
1365
|
+
attachEventListeners() {
|
|
1366
|
+
if (this.state.productData) {
|
|
1367
|
+
this.eventManager.attachLinkListeners(
|
|
1368
|
+
this.shadowRoot,
|
|
1369
|
+
this.state.productData,
|
|
1370
|
+
this.ean
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Utilitaires
|
|
1376
|
+
getVIPointsImageUrl() {
|
|
1377
|
+
const baseUrl = (this.apiBaseUrl || '').replace('/api', '');
|
|
1378
|
+
return `${baseUrl}/api/sdk/assets/image-vipoints.jpg`
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
triggerBackgroundSync() {
|
|
1382
|
+
const sdk = this.getSDKInstance();
|
|
1383
|
+
if (sdk?.syncService) {
|
|
1384
|
+
setTimeout(() => {
|
|
1385
|
+
this.log(`[ViprosOfferCard] Background sync for EAN ${this.ean}`);
|
|
1386
|
+
sdk.syncProductInBackground(this.ean, window.location.href);
|
|
1387
|
+
}, 100);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Nettoyage
|
|
1392
|
+
cleanup() {
|
|
1393
|
+
this.eventManager.cleanup();
|
|
1394
|
+
this.state.apiClient = null;
|
|
1395
|
+
this.state.productData = null;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Référence SDK (méthodes statiques de l'ancien composant)
|
|
1400
|
+
ViprosOfferCard.setSDKInstance = function(sdk) {
|
|
1401
|
+
if (sdk?.config?.debug) {
|
|
1402
|
+
console.log('[ViprosOfferCard] SDK instance connected');
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
class ConfigManager {
|
|
1407
|
+
static create (userConfig = {}) {
|
|
1408
|
+
const defaultConfig = ConfigManager.getDefaultConfig();
|
|
1409
|
+
const mergedConfig = ConfigManager.mergeConfig(defaultConfig, userConfig);
|
|
1410
|
+
const validatedConfig = ConfigManager.validateConfig(mergedConfig);
|
|
1411
|
+
|
|
1412
|
+
return validatedConfig
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
static getDefaultConfig () {
|
|
1416
|
+
return {
|
|
1417
|
+
// API Configuration
|
|
1418
|
+
apiBaseUrl: ConfigManager.detectApiBaseUrl(),
|
|
1419
|
+
apiKey: null, // Clé API obligatoire - doit être fournie explicitement
|
|
1420
|
+
timeout: 10000,
|
|
1421
|
+
|
|
1422
|
+
// EAN Configuration
|
|
1423
|
+
ean: null, // EAN par défaut pour les composants sans attribut ean
|
|
1424
|
+
|
|
1425
|
+
// Global Component Defaults
|
|
1426
|
+
price: null, // Prix par défaut pour les composants sans attribut price
|
|
1427
|
+
primaryColor: null, // Couleur principale par défaut pour les composants sans attribut primary-color
|
|
1428
|
+
textColor: null, // Couleur de texte par défaut pour les composants sans attribut text-color
|
|
1429
|
+
backgroundColor: null, // Couleur de fond par défaut pour les composants sans attribut background-color
|
|
1430
|
+
cardBonusBg: null, // Couleur de fond des badges bonus par défaut pour les composants sans attribut card-bonus-bg
|
|
1431
|
+
cardBonusColor: null, // Couleur de texte/icône des badges bonus par défaut pour les composants sans attribut card-bonus-color
|
|
1432
|
+
|
|
1433
|
+
// Cache Configuration
|
|
1434
|
+
cacheTimeout: 300000, // 5 minutes
|
|
1435
|
+
cachePrefix: 'vipros_sdk_',
|
|
1436
|
+
maxCacheSize: 100,
|
|
1437
|
+
|
|
1438
|
+
// Container Configuration
|
|
1439
|
+
containerSelector: '[data-ean]',
|
|
1440
|
+
eanAttribute: 'data-ean',
|
|
1441
|
+
|
|
1442
|
+
// Rendering Configuration
|
|
1443
|
+
renderPosition: 'replace', // 'before', 'after', 'prepend', 'append', 'replace'
|
|
1444
|
+
excludeSelectors: ['.vipros-offer', '[data-vipros-exclude]'],
|
|
1445
|
+
|
|
1446
|
+
// Template Configuration - Templates par défaut seront utilisés par TemplateEngine
|
|
1447
|
+
templates: {
|
|
1448
|
+
// Ne pas spécifier de valeurs par défaut - TemplateEngine a ses propres templates intégrés
|
|
1449
|
+
},
|
|
1450
|
+
|
|
1451
|
+
// Styling Configuration
|
|
1452
|
+
styling: {
|
|
1453
|
+
theme: 'minimal', // 'minimal', 'branded', 'custom'
|
|
1454
|
+
customCSS: {},
|
|
1455
|
+
inlineStyles: true,
|
|
1456
|
+
cssPrefix: 'vipros-',
|
|
1457
|
+
responsive: true
|
|
1458
|
+
},
|
|
1459
|
+
|
|
1460
|
+
// Performance Configuration
|
|
1461
|
+
batchRequests: true,
|
|
1462
|
+
lazyLoad: true,
|
|
1463
|
+
maxConcurrentRequests: 3,
|
|
1464
|
+
debounceDelay: 300,
|
|
1465
|
+
|
|
1466
|
+
// Behavior Configuration
|
|
1467
|
+
hideOnError: false,
|
|
1468
|
+
retryOnError: true,
|
|
1469
|
+
gracefulDegradetion: true,
|
|
1470
|
+
|
|
1471
|
+
// Sync Configuration
|
|
1472
|
+
syncEnabled: true,
|
|
1473
|
+
syncOptions: {
|
|
1474
|
+
cooldownTime: 24 * 60 * 60 * 1000, // 24 heures - une sync par jour par produit
|
|
1475
|
+
syncDelay: 100, // Délai avant sync en ms
|
|
1476
|
+
maxRetries: 3,
|
|
1477
|
+
retryDelay: 1000 // Délai entre retry en ms
|
|
1478
|
+
},
|
|
1479
|
+
|
|
1480
|
+
// Debug Configuration
|
|
1481
|
+
debug: false,
|
|
1482
|
+
verbose: false,
|
|
1483
|
+
logLevel: 'warn', // 'error', 'warn', 'info', 'debug'
|
|
1484
|
+
|
|
1485
|
+
// Event Callbacks
|
|
1486
|
+
onReady: null,
|
|
1487
|
+
onOfferFound: null,
|
|
1488
|
+
onOfferRendered: null,
|
|
1489
|
+
onOfferLoaded: null, // Nouveau callback quand une offre est complètement chargée et affichée
|
|
1490
|
+
onError: null,
|
|
1491
|
+
|
|
1492
|
+
// Feature Flags
|
|
1493
|
+
features: {
|
|
1494
|
+
analyticsTracking: false,
|
|
1495
|
+
performanceMonitoring: false,
|
|
1496
|
+
a11yEnhancements: true,
|
|
1497
|
+
seoOptimizations: true
|
|
1498
|
+
},
|
|
1499
|
+
|
|
1500
|
+
// Internationalization
|
|
1501
|
+
locale: 'fr-FR',
|
|
1502
|
+
currency: 'EUR',
|
|
1503
|
+
dateFormat: 'DD/MM/YYYY',
|
|
1504
|
+
|
|
1505
|
+
// Version
|
|
1506
|
+
version: '1.0.0'
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
static mergeConfig (defaultConfig, userConfig) {
|
|
1511
|
+
return ConfigManager.deepMerge(defaultConfig, userConfig)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
static deepMerge (target, source) {
|
|
1515
|
+
const result = { ...target };
|
|
1516
|
+
|
|
1517
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1518
|
+
if (value === null || value === undefined) {
|
|
1519
|
+
continue
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (ConfigManager.isObject(value) && ConfigManager.isObject(result[key])) {
|
|
1523
|
+
result[key] = ConfigManager.deepMerge(result[key], value);
|
|
1524
|
+
} else {
|
|
1525
|
+
result[key] = value;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return result
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
static isObject (item) {
|
|
1533
|
+
return item && typeof item === 'object' && !Array.isArray(item)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
static validateConfig (config) {
|
|
1537
|
+
const errors = [];
|
|
1538
|
+
|
|
1539
|
+
// Validate API Configuration
|
|
1540
|
+
if (!config.apiBaseUrl || typeof config.apiBaseUrl !== 'string') {
|
|
1541
|
+
errors.push('apiBaseUrl must be a valid string');
|
|
1542
|
+
} else {
|
|
1543
|
+
try {
|
|
1544
|
+
new URL(config.apiBaseUrl);
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
errors.push('apiBaseUrl must be a valid URL');
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (!config.apiKey) {
|
|
1551
|
+
errors.push('apiKey is required');
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
if (config.apiKey && typeof config.apiKey !== 'string') {
|
|
1555
|
+
errors.push('apiKey must be a string');
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
if (config.timeout && (typeof config.timeout !== 'number' || config.timeout <= 0)) {
|
|
1559
|
+
errors.push('timeout must be a positive number');
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// Validate Cache Configuration
|
|
1563
|
+
if (config.cacheTimeout && (typeof config.cacheTimeout !== 'number' || config.cacheTimeout < 0)) {
|
|
1564
|
+
errors.push('cacheTimeout must be a non-negative number');
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (config.maxCacheSize && (typeof config.maxCacheSize !== 'number' || config.maxCacheSize <= 0)) {
|
|
1568
|
+
errors.push('maxCacheSize must be a positive number');
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Validate Container Configuration
|
|
1572
|
+
if (config.containerSelector && typeof config.containerSelector !== 'string') {
|
|
1573
|
+
errors.push('containerSelector must be a string');
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (config.eanAttribute && typeof config.eanAttribute !== 'string') {
|
|
1577
|
+
errors.push('eanAttribute must be a string');
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// Validate Styling
|
|
1581
|
+
if (config.styling) {
|
|
1582
|
+
const validThemes = ['minimal', 'branded', 'custom'];
|
|
1583
|
+
if (config.styling.theme && !validThemes.includes(config.styling.theme)) {
|
|
1584
|
+
errors.push(`styling.theme must be one of: ${validThemes.join(', ')}`);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (config.styling.customCSS && !ConfigManager.isObject(config.styling.customCSS)) {
|
|
1588
|
+
errors.push('styling.customCSS must be an object');
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Validate Performance Configuration
|
|
1593
|
+
if (config.maxConcurrentRequests && (typeof config.maxConcurrentRequests !== 'number' || config.maxConcurrentRequests <= 0)) {
|
|
1594
|
+
errors.push('maxConcurrentRequests must be a positive number');
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (config.debounceDelay && (typeof config.debounceDelay !== 'number' || config.debounceDelay < 0)) {
|
|
1598
|
+
errors.push('debounceDelay must be a non-negative number');
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
// Validate Log Level
|
|
1603
|
+
const validLogLevels = ['error', 'warn', 'info', 'debug'];
|
|
1604
|
+
if (config.logLevel && !validLogLevels.includes(config.logLevel)) {
|
|
1605
|
+
errors.push(`logLevel must be one of: ${validLogLevels.join(', ')}`);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Validate Event Callbacks
|
|
1609
|
+
const eventCallbacks = ['onReady', 'onOfferFound', 'onOfferRendered', 'onOfferLoaded', 'onError'];
|
|
1610
|
+
for (const callback of eventCallbacks) {
|
|
1611
|
+
if (config[callback] && typeof config[callback] !== 'function') {
|
|
1612
|
+
errors.push(`${callback} must be a function`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Validate Locale
|
|
1617
|
+
if (config.locale && typeof config.locale !== 'string') {
|
|
1618
|
+
errors.push('locale must be a string');
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Validate Currency
|
|
1622
|
+
if (config.currency && typeof config.currency !== 'string') {
|
|
1623
|
+
errors.push('currency must be a string');
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
if (errors.length > 0) {
|
|
1627
|
+
const error = new Error(`Configuration validation failed:\n${errors.join('\n')}`);
|
|
1628
|
+
error.validationErrors = errors;
|
|
1629
|
+
throw error
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Apply post-validation processing
|
|
1633
|
+
return ConfigManager.processConfig(config)
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
static processConfig (config) {
|
|
1637
|
+
const processed = { ...config };
|
|
1638
|
+
|
|
1639
|
+
// Normalize API Base URL
|
|
1640
|
+
processed.apiBaseUrl = processed.apiBaseUrl.replace(/\/$/, '');
|
|
1641
|
+
|
|
1642
|
+
// Ensure containerSelector is properly formatted
|
|
1643
|
+
if (!processed.containerSelector.startsWith('[') && !processed.containerSelector.startsWith('.') && !processed.containerSelector.startsWith('#')) {
|
|
1644
|
+
processed.containerSelector = `[${processed.eanAttribute}]`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Set debug flags based on logLevel
|
|
1648
|
+
if (processed.logLevel === 'debug') {
|
|
1649
|
+
processed.debug = true;
|
|
1650
|
+
processed.verbose = true;
|
|
1651
|
+
} else if (processed.logLevel === 'info') {
|
|
1652
|
+
processed.debug = true;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Validate and normalize CSS custom properties
|
|
1656
|
+
if (processed.styling.customCSS) {
|
|
1657
|
+
const normalizedCSS = {};
|
|
1658
|
+
for (const [key, value] of Object.entries(processed.styling.customCSS)) {
|
|
1659
|
+
const cssProperty = key.startsWith('--') ? key : `--vipros-${key}`;
|
|
1660
|
+
normalizedCSS[cssProperty] = value;
|
|
1661
|
+
}
|
|
1662
|
+
processed.styling.customCSS = normalizedCSS;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Set cache prefix with version
|
|
1666
|
+
processed.cachePrefix = `${processed.cachePrefix}v${processed.version.replace(/\./g, '_')}_`;
|
|
1667
|
+
|
|
1668
|
+
// Calculate derived values
|
|
1669
|
+
processed.derived = {
|
|
1670
|
+
isProduction: !processed.debug,
|
|
1671
|
+
shouldCache: processed.cacheTimeout > 0,
|
|
1672
|
+
shouldBatch: processed.batchRequests && processed.maxConcurrentRequests > 1,
|
|
1673
|
+
shouldLazyLoad: processed.lazyLoad,
|
|
1674
|
+
hasCustomStyling: processed.styling.theme === 'custom' || Object.keys(processed.styling.customCSS || {}).length > 0
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
return processed
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
|
|
1681
|
+
static createSecureConfig (userConfig, serverConfig = {}) {
|
|
1682
|
+
const clientSafeServerConfig = {
|
|
1683
|
+
apiBaseUrl: serverConfig.apiBaseUrl,
|
|
1684
|
+
allowedFeatures: serverConfig.allowedFeatures,
|
|
1685
|
+
rateLimits: serverConfig.rateLimits,
|
|
1686
|
+
version: serverConfig.version
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
return ConfigManager.create({
|
|
1690
|
+
...clientSafeServerConfig,
|
|
1691
|
+
...userConfig
|
|
1692
|
+
})
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
static validateRuntimeConfig (config) {
|
|
1696
|
+
if (!config) {
|
|
1697
|
+
throw new Error('Configuration is required')
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (!config.derived) {
|
|
1701
|
+
throw new Error('Configuration has not been processed')
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
return true
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
static getConfigSummary (config) {
|
|
1708
|
+
return {
|
|
1709
|
+
version: config.version,
|
|
1710
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
1711
|
+
theme: config.styling.theme,
|
|
1712
|
+
cacheEnabled: config.derived.shouldCache,
|
|
1713
|
+
debugMode: config.debug,
|
|
1714
|
+
features: Object.keys(config.features).filter(key => config.features[key])
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Auto-détection de l'URL d'API selon le domaine
|
|
1720
|
+
* SINGLE SOURCE OF TRUTH pour la détection d'environnement
|
|
1721
|
+
* Enhanced with robust error handling for edge cases
|
|
1722
|
+
*/
|
|
1723
|
+
static detectApiBaseUrl(debug = false) {
|
|
1724
|
+
try {
|
|
1725
|
+
// Edge case: No window object (SSR, Node.js, service worker)
|
|
1726
|
+
if (typeof window === 'undefined') {
|
|
1727
|
+
if (debug) console.warn('[ConfigManager] No window object - defaulting to production API');
|
|
1728
|
+
return 'https://msys.vipros.fr/api'
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Edge case: window.location is not available or corrupted
|
|
1732
|
+
if (!window.location || typeof window.location.hostname !== 'string') {
|
|
1733
|
+
if (debug) console.warn('[ConfigManager] Invalid window.location - defaulting to production API');
|
|
1734
|
+
return 'https://msys.vipros.fr/api'
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
const protocol = window.location.protocol || 'https:';
|
|
1738
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
1739
|
+
|
|
1740
|
+
// Edge case: Empty or invalid hostname
|
|
1741
|
+
if (!hostname || hostname.length === 0) {
|
|
1742
|
+
if (debug) console.warn('[ConfigManager] Empty hostname - defaulting to production API');
|
|
1743
|
+
return 'https://msys.vipros.fr/api'
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Edge case: Local development environments
|
|
1747
|
+
if (protocol === 'file:' || hostname === 'localhost' || hostname.startsWith('127.0.0.1') || hostname.startsWith('0.0.0.0')) {
|
|
1748
|
+
if (debug) console.log('[ConfigManager] Local development detected - using DDEV fallback');
|
|
1749
|
+
return 'https://vipros-connect.ddev.site/api'
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Edge case: Non-standard ports for development
|
|
1753
|
+
if (hostname.includes(':') && (hostname.includes('localhost') || hostname.includes('127.0.0.1'))) {
|
|
1754
|
+
if (debug) console.log('[ConfigManager] Local development with port detected - using DDEV fallback');
|
|
1755
|
+
return 'https://vipros-connect.ddev.site/api'
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Correspondance directe des domaines
|
|
1759
|
+
if (hostname === 'vipros-connect.ddev.site') {
|
|
1760
|
+
return `${protocol}//${hostname}/api`
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (hostname === 'msys.preprod.vipros.fr') {
|
|
1764
|
+
return `${protocol}//msys.preprod.vipros.fr/api`
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Edge case: Enterprise subdomains or custom VIPros domains
|
|
1768
|
+
if (hostname.endsWith('.vipros.fr')) {
|
|
1769
|
+
if (debug) console.log(`[ConfigManager] VIPros subdomain detected: ${hostname}`);
|
|
1770
|
+
return `${protocol}//${hostname}/api`
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Edge case: IP addresses (development/staging environments)
|
|
1774
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
|
1775
|
+
if (debug) console.log(`[ConfigManager] IP address detected: ${hostname} - using production API`);
|
|
1776
|
+
return 'https://msys.vipros.fr/api'
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Par défaut : production (toujours HTTPS pour la sécurité)
|
|
1780
|
+
if (debug) console.log(`[ConfigManager] Unknown domain: ${hostname} - defaulting to production API`);
|
|
1781
|
+
return 'https://msys.vipros.fr/api'
|
|
1782
|
+
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
// Edge case: Unexpected error during detection
|
|
1785
|
+
if (debug) console.error(`[ConfigManager] Error during API base URL detection: ${error.message} - defaulting to production`);
|
|
1786
|
+
return 'https://msys.vipros.fr/api'
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
class EventEmitter {
|
|
1792
|
+
constructor () {
|
|
1793
|
+
this.events = new Map();
|
|
1794
|
+
this.maxListeners = 10;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
on (event, callback) {
|
|
1798
|
+
if (typeof callback !== 'function') {
|
|
1799
|
+
throw new TypeError('Callback must be a function')
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
if (!this.events.has(event)) {
|
|
1803
|
+
this.events.set(event, []);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const listeners = this.events.get(event);
|
|
1807
|
+
|
|
1808
|
+
if (listeners.length >= this.maxListeners) {
|
|
1809
|
+
console.warn(`Max listeners (${this.maxListeners}) exceeded for event: ${event}`);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
listeners.push(callback);
|
|
1813
|
+
return this
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
once (event, callback) {
|
|
1817
|
+
if (typeof callback !== 'function') {
|
|
1818
|
+
throw new TypeError('Callback must be a function')
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const onceWrapper = (...args) => {
|
|
1822
|
+
this.off(event, onceWrapper);
|
|
1823
|
+
callback.apply(this, args);
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
return this.on(event, onceWrapper)
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
off (event, callback) {
|
|
1830
|
+
if (!this.events.has(event)) {
|
|
1831
|
+
return this
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
if (!callback) {
|
|
1835
|
+
this.events.delete(event);
|
|
1836
|
+
return this
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const listeners = this.events.get(event);
|
|
1840
|
+
const index = listeners.indexOf(callback);
|
|
1841
|
+
|
|
1842
|
+
if (index !== -1) {
|
|
1843
|
+
listeners.splice(index, 1);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (listeners.length === 0) {
|
|
1847
|
+
this.events.delete(event);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
return this
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
emit (event, ...args) {
|
|
1854
|
+
if (!this.events.has(event)) {
|
|
1855
|
+
return false
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
const listeners = this.events.get(event).slice();
|
|
1859
|
+
|
|
1860
|
+
for (const callback of listeners) {
|
|
1861
|
+
try {
|
|
1862
|
+
callback.apply(this, args);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
console.error(`Error in event listener for '${event}':`, error);
|
|
1865
|
+
this.emit('error', error);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
return true
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
removeAllListeners (event = null) {
|
|
1873
|
+
if (event) {
|
|
1874
|
+
this.events.delete(event);
|
|
1875
|
+
} else {
|
|
1876
|
+
this.events.clear();
|
|
1877
|
+
}
|
|
1878
|
+
return this
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
listenerCount (event) {
|
|
1882
|
+
return this.events.has(event) ? this.events.get(event).length : 0
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
getMaxListeners () {
|
|
1886
|
+
return this.maxListeners
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
setMaxListeners (max) {
|
|
1890
|
+
if (typeof max !== 'number' || max < 0) {
|
|
1891
|
+
throw new TypeError('Max listeners must be a non-negative number')
|
|
1892
|
+
}
|
|
1893
|
+
this.maxListeners = max;
|
|
1894
|
+
return this
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
eventNames () {
|
|
1898
|
+
return Array.from(this.events.keys())
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
listeners (event) {
|
|
1902
|
+
return this.events.has(event) ? this.events.get(event).slice() : []
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// DEFLATE is a complex format; to read this code, you should probably check the RFC first:
|
|
1907
|
+
// https://tools.ietf.org/html/rfc1951
|
|
1908
|
+
// You may also wish to take a look at the guide I made about this program:
|
|
1909
|
+
// https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
|
|
1910
|
+
// Some of the following code is similar to that of UZIP.js:
|
|
1911
|
+
// https://github.com/photopea/UZIP.js
|
|
1912
|
+
// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
|
|
1913
|
+
// Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
|
|
1914
|
+
// is better for memory in most engines (I *think*).
|
|
1915
|
+
|
|
1916
|
+
// aliases for shorter compressed code (most minifers don't do this)
|
|
1917
|
+
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
|
|
1918
|
+
// fixed length extra bits
|
|
1919
|
+
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
|
|
1920
|
+
// fixed distance extra bits
|
|
1921
|
+
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
|
|
1922
|
+
// code length index map
|
|
1923
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
1924
|
+
// get base, reverse index map from extra bits
|
|
1925
|
+
var freb = function (eb, start) {
|
|
1926
|
+
var b = new u16(31);
|
|
1927
|
+
for (var i = 0; i < 31; ++i) {
|
|
1928
|
+
b[i] = start += 1 << eb[i - 1];
|
|
1929
|
+
}
|
|
1930
|
+
// numbers here are at max 18 bits
|
|
1931
|
+
var r = new i32(b[30]);
|
|
1932
|
+
for (var i = 1; i < 30; ++i) {
|
|
1933
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
1934
|
+
r[j] = ((j - b[i]) << 5) | i;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
return { b: b, r: r };
|
|
1938
|
+
};
|
|
1939
|
+
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
|
|
1940
|
+
// we can ignore the fact that the other numbers are wrong; they never happen anyway
|
|
1941
|
+
fl[28] = 258, revfl[258] = 28;
|
|
1942
|
+
var _b = freb(fdeb, 0), fd = _b.b;
|
|
1943
|
+
// map of value to reverse (assuming 16 bits)
|
|
1944
|
+
var rev = new u16(32768);
|
|
1945
|
+
for (var i = 0; i < 32768; ++i) {
|
|
1946
|
+
// reverse table algorithm from SO
|
|
1947
|
+
var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
|
|
1948
|
+
x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
|
|
1949
|
+
x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
|
|
1950
|
+
rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
|
|
1951
|
+
}
|
|
1952
|
+
// create huffman tree from u8 "map": index -> code length for code index
|
|
1953
|
+
// mb (max bits) must be at most 15
|
|
1954
|
+
// TODO: optimize/split up?
|
|
1955
|
+
var hMap = (function (cd, mb, r) {
|
|
1956
|
+
var s = cd.length;
|
|
1957
|
+
// index
|
|
1958
|
+
var i = 0;
|
|
1959
|
+
// u16 "map": index -> # of codes with bit length = index
|
|
1960
|
+
var l = new u16(mb);
|
|
1961
|
+
// length of cd must be 288 (total # of codes)
|
|
1962
|
+
for (; i < s; ++i) {
|
|
1963
|
+
if (cd[i])
|
|
1964
|
+
++l[cd[i] - 1];
|
|
1965
|
+
}
|
|
1966
|
+
// u16 "map": index -> minimum code for bit length = index
|
|
1967
|
+
var le = new u16(mb);
|
|
1968
|
+
for (i = 1; i < mb; ++i) {
|
|
1969
|
+
le[i] = (le[i - 1] + l[i - 1]) << 1;
|
|
1970
|
+
}
|
|
1971
|
+
var co;
|
|
1972
|
+
if (r) {
|
|
1973
|
+
// u16 "map": index -> number of actual bits, symbol for code
|
|
1974
|
+
co = new u16(1 << mb);
|
|
1975
|
+
// bits to remove for reverser
|
|
1976
|
+
var rvb = 15 - mb;
|
|
1977
|
+
for (i = 0; i < s; ++i) {
|
|
1978
|
+
// ignore 0 lengths
|
|
1979
|
+
if (cd[i]) {
|
|
1980
|
+
// num encoding both symbol and bits read
|
|
1981
|
+
var sv = (i << 4) | cd[i];
|
|
1982
|
+
// free bits
|
|
1983
|
+
var r_1 = mb - cd[i];
|
|
1984
|
+
// start value
|
|
1985
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
1986
|
+
// m is end value
|
|
1987
|
+
for (var m = v | ((1 << r_1) - 1); v <= m; ++v) {
|
|
1988
|
+
// every 16 bit value starting with the code yields the same result
|
|
1989
|
+
co[rev[v] >> rvb] = sv;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
else {
|
|
1995
|
+
co = new u16(s);
|
|
1996
|
+
for (i = 0; i < s; ++i) {
|
|
1997
|
+
if (cd[i]) {
|
|
1998
|
+
co[i] = rev[le[cd[i] - 1]++] >> (15 - cd[i]);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return co;
|
|
2003
|
+
});
|
|
2004
|
+
// fixed length tree
|
|
2005
|
+
var flt = new u8(288);
|
|
2006
|
+
for (var i = 0; i < 144; ++i)
|
|
2007
|
+
flt[i] = 8;
|
|
2008
|
+
for (var i = 144; i < 256; ++i)
|
|
2009
|
+
flt[i] = 9;
|
|
2010
|
+
for (var i = 256; i < 280; ++i)
|
|
2011
|
+
flt[i] = 7;
|
|
2012
|
+
for (var i = 280; i < 288; ++i)
|
|
2013
|
+
flt[i] = 8;
|
|
2014
|
+
// fixed distance tree
|
|
2015
|
+
var fdt = new u8(32);
|
|
2016
|
+
for (var i = 0; i < 32; ++i)
|
|
2017
|
+
fdt[i] = 5;
|
|
2018
|
+
// fixed length map
|
|
2019
|
+
var flrm = /*#__PURE__*/ hMap(flt, 9, 1);
|
|
2020
|
+
// fixed distance map
|
|
2021
|
+
var fdrm = /*#__PURE__*/ hMap(fdt, 5, 1);
|
|
2022
|
+
// find max of array
|
|
2023
|
+
var max = function (a) {
|
|
2024
|
+
var m = a[0];
|
|
2025
|
+
for (var i = 1; i < a.length; ++i) {
|
|
2026
|
+
if (a[i] > m)
|
|
2027
|
+
m = a[i];
|
|
2028
|
+
}
|
|
2029
|
+
return m;
|
|
2030
|
+
};
|
|
2031
|
+
// read d, starting at bit p and mask with m
|
|
2032
|
+
var bits = function (d, p, m) {
|
|
2033
|
+
var o = (p / 8) | 0;
|
|
2034
|
+
return ((d[o] | (d[o + 1] << 8)) >> (p & 7)) & m;
|
|
2035
|
+
};
|
|
2036
|
+
// read d, starting at bit p continuing for at least 16 bits
|
|
2037
|
+
var bits16 = function (d, p) {
|
|
2038
|
+
var o = (p / 8) | 0;
|
|
2039
|
+
return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >> (p & 7));
|
|
2040
|
+
};
|
|
2041
|
+
// get end of byte
|
|
2042
|
+
var shft = function (p) { return ((p + 7) / 8) | 0; };
|
|
2043
|
+
// typed array slice - allows garbage collector to free original reference,
|
|
2044
|
+
// while being more compatible than .slice
|
|
2045
|
+
var slc = function (v, s, e) {
|
|
2046
|
+
if (e == null || e > v.length)
|
|
2047
|
+
e = v.length;
|
|
2048
|
+
// can't use .constructor in case user-supplied
|
|
2049
|
+
return new u8(v.subarray(s, e));
|
|
2050
|
+
};
|
|
2051
|
+
// error codes
|
|
2052
|
+
var ec = [
|
|
2053
|
+
'unexpected EOF',
|
|
2054
|
+
'invalid block type',
|
|
2055
|
+
'invalid length/literal',
|
|
2056
|
+
'invalid distance',
|
|
2057
|
+
'stream finished',
|
|
2058
|
+
'no stream handler',
|
|
2059
|
+
,
|
|
2060
|
+
'no callback',
|
|
2061
|
+
'invalid UTF-8 data',
|
|
2062
|
+
'extra field too long',
|
|
2063
|
+
'date not in range 1980-2099',
|
|
2064
|
+
'filename too long',
|
|
2065
|
+
'stream finishing',
|
|
2066
|
+
'invalid zip data'
|
|
2067
|
+
// determined by unknown compression method
|
|
2068
|
+
];
|
|
2069
|
+
var err = function (ind, msg, nt) {
|
|
2070
|
+
var e = new Error(msg || ec[ind]);
|
|
2071
|
+
e.code = ind;
|
|
2072
|
+
if (Error.captureStackTrace)
|
|
2073
|
+
Error.captureStackTrace(e, err);
|
|
2074
|
+
if (!nt)
|
|
2075
|
+
throw e;
|
|
2076
|
+
return e;
|
|
2077
|
+
};
|
|
2078
|
+
// expands raw DEFLATE data
|
|
2079
|
+
var inflt = function (dat, st, buf, dict) {
|
|
2080
|
+
// source length dict length
|
|
2081
|
+
var sl = dat.length, dl = 0;
|
|
2082
|
+
if (!sl || st.f && !st.l)
|
|
2083
|
+
return buf || new u8(0);
|
|
2084
|
+
var noBuf = !buf;
|
|
2085
|
+
// have to estimate size
|
|
2086
|
+
var resize = noBuf || st.i != 2;
|
|
2087
|
+
// no state
|
|
2088
|
+
var noSt = st.i;
|
|
2089
|
+
// Assumes roughly 33% compression ratio average
|
|
2090
|
+
if (noBuf)
|
|
2091
|
+
buf = new u8(sl * 3);
|
|
2092
|
+
// ensure buffer can fit at least l elements
|
|
2093
|
+
var cbuf = function (l) {
|
|
2094
|
+
var bl = buf.length;
|
|
2095
|
+
// need to increase size to fit
|
|
2096
|
+
if (l > bl) {
|
|
2097
|
+
// Double or set to necessary, whichever is greater
|
|
2098
|
+
var nbuf = new u8(Math.max(bl * 2, l));
|
|
2099
|
+
nbuf.set(buf);
|
|
2100
|
+
buf = nbuf;
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
// last chunk bitpos bytes
|
|
2104
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
2105
|
+
// total bits
|
|
2106
|
+
var tbts = sl * 8;
|
|
2107
|
+
do {
|
|
2108
|
+
if (!lm) {
|
|
2109
|
+
// BFINAL - this is only 1 when last chunk is next
|
|
2110
|
+
final = bits(dat, pos, 1);
|
|
2111
|
+
// type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman
|
|
2112
|
+
var type = bits(dat, pos + 1, 3);
|
|
2113
|
+
pos += 3;
|
|
2114
|
+
if (!type) {
|
|
2115
|
+
// go to end of byte boundary
|
|
2116
|
+
var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l;
|
|
2117
|
+
if (t > sl) {
|
|
2118
|
+
if (noSt)
|
|
2119
|
+
err(0);
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
// ensure size
|
|
2123
|
+
if (resize)
|
|
2124
|
+
cbuf(bt + l);
|
|
2125
|
+
// Copy over uncompressed data
|
|
2126
|
+
buf.set(dat.subarray(s, t), bt);
|
|
2127
|
+
// Get new bitpos, update byte count
|
|
2128
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
else if (type == 1)
|
|
2132
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
2133
|
+
else if (type == 2) {
|
|
2134
|
+
// literal lengths
|
|
2135
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
2136
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
2137
|
+
pos += 14;
|
|
2138
|
+
// length+distance tree
|
|
2139
|
+
var ldt = new u8(tl);
|
|
2140
|
+
// code length tree
|
|
2141
|
+
var clt = new u8(19);
|
|
2142
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
2143
|
+
// use index map to get real code
|
|
2144
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
2145
|
+
}
|
|
2146
|
+
pos += hcLen * 3;
|
|
2147
|
+
// code lengths bits
|
|
2148
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
2149
|
+
// code lengths map
|
|
2150
|
+
var clm = hMap(clt, clb, 1);
|
|
2151
|
+
for (var i = 0; i < tl;) {
|
|
2152
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
2153
|
+
// bits read
|
|
2154
|
+
pos += r & 15;
|
|
2155
|
+
// symbol
|
|
2156
|
+
var s = r >> 4;
|
|
2157
|
+
// code length to copy
|
|
2158
|
+
if (s < 16) {
|
|
2159
|
+
ldt[i++] = s;
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
// copy count
|
|
2163
|
+
var c = 0, n = 0;
|
|
2164
|
+
if (s == 16)
|
|
2165
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
2166
|
+
else if (s == 17)
|
|
2167
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
2168
|
+
else if (s == 18)
|
|
2169
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
2170
|
+
while (n--)
|
|
2171
|
+
ldt[i++] = c;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
// length tree distance tree
|
|
2175
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
2176
|
+
// max length bits
|
|
2177
|
+
lbt = max(lt);
|
|
2178
|
+
// max dist bits
|
|
2179
|
+
dbt = max(dt);
|
|
2180
|
+
lm = hMap(lt, lbt, 1);
|
|
2181
|
+
dm = hMap(dt, dbt, 1);
|
|
2182
|
+
}
|
|
2183
|
+
else
|
|
2184
|
+
err(1);
|
|
2185
|
+
if (pos > tbts) {
|
|
2186
|
+
if (noSt)
|
|
2187
|
+
err(0);
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
// Make sure the buffer can hold this + the largest possible addition
|
|
2192
|
+
// Maximum chunk size (practically, theoretically infinite) is 2^17
|
|
2193
|
+
if (resize)
|
|
2194
|
+
cbuf(bt + 131072);
|
|
2195
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
2196
|
+
var lpos = pos;
|
|
2197
|
+
for (;; lpos = pos) {
|
|
2198
|
+
// bits read, code
|
|
2199
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
2200
|
+
pos += c & 15;
|
|
2201
|
+
if (pos > tbts) {
|
|
2202
|
+
if (noSt)
|
|
2203
|
+
err(0);
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
if (!c)
|
|
2207
|
+
err(2);
|
|
2208
|
+
if (sym < 256)
|
|
2209
|
+
buf[bt++] = sym;
|
|
2210
|
+
else if (sym == 256) {
|
|
2211
|
+
lpos = pos, lm = null;
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
else {
|
|
2215
|
+
var add = sym - 254;
|
|
2216
|
+
// no extra bits needed if less
|
|
2217
|
+
if (sym > 264) {
|
|
2218
|
+
// index
|
|
2219
|
+
var i = sym - 257, b = fleb[i];
|
|
2220
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
2221
|
+
pos += b;
|
|
2222
|
+
}
|
|
2223
|
+
// dist
|
|
2224
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
2225
|
+
if (!d)
|
|
2226
|
+
err(3);
|
|
2227
|
+
pos += d & 15;
|
|
2228
|
+
var dt = fd[dsym];
|
|
2229
|
+
if (dsym > 3) {
|
|
2230
|
+
var b = fdeb[dsym];
|
|
2231
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
2232
|
+
}
|
|
2233
|
+
if (pos > tbts) {
|
|
2234
|
+
if (noSt)
|
|
2235
|
+
err(0);
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
if (resize)
|
|
2239
|
+
cbuf(bt + 131072);
|
|
2240
|
+
var end = bt + add;
|
|
2241
|
+
if (bt < dt) {
|
|
2242
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
2243
|
+
if (shift + bt < 0)
|
|
2244
|
+
err(3);
|
|
2245
|
+
for (; bt < dend; ++bt)
|
|
2246
|
+
buf[bt] = dict[shift + bt];
|
|
2247
|
+
}
|
|
2248
|
+
for (; bt < end; ++bt)
|
|
2249
|
+
buf[bt] = buf[bt - dt];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
2253
|
+
if (lm)
|
|
2254
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
2255
|
+
} while (!final);
|
|
2256
|
+
// don't reallocate for streams or user buffers
|
|
2257
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
2258
|
+
};
|
|
2259
|
+
// empty
|
|
2260
|
+
var et = /*#__PURE__*/ new u8(0);
|
|
2261
|
+
// gzip footer: -8 to -4 = CRC, -4 to -0 is length
|
|
2262
|
+
// gzip start
|
|
2263
|
+
var gzs = function (d) {
|
|
2264
|
+
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
|
2265
|
+
err(6, 'invalid gzip data');
|
|
2266
|
+
var flg = d[3];
|
|
2267
|
+
var st = 10;
|
|
2268
|
+
if (flg & 4)
|
|
2269
|
+
st += (d[10] | d[11] << 8) + 2;
|
|
2270
|
+
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
|
2271
|
+
;
|
|
2272
|
+
return st + (flg & 2);
|
|
2273
|
+
};
|
|
2274
|
+
// gzip length
|
|
2275
|
+
var gzl = function (d) {
|
|
2276
|
+
var l = d.length;
|
|
2277
|
+
return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
|
|
2278
|
+
};
|
|
2279
|
+
// zlib start
|
|
2280
|
+
var zls = function (d, dict) {
|
|
2281
|
+
if ((d[0] & 15) != 8 || (d[0] >> 4) > 7 || ((d[0] << 8 | d[1]) % 31))
|
|
2282
|
+
err(6, 'invalid zlib data');
|
|
2283
|
+
if ((d[1] >> 5 & 1) == 1)
|
|
2284
|
+
err(6, 'invalid zlib data: ' + (d[1] & 32 ? 'need' : 'unexpected') + ' dictionary');
|
|
2285
|
+
return (d[1] >> 3 & 4) + 2;
|
|
2286
|
+
};
|
|
2287
|
+
/**
|
|
2288
|
+
* Expands GZIP data
|
|
2289
|
+
* @param data The data to decompress
|
|
2290
|
+
* @param opts The decompression options
|
|
2291
|
+
* @returns The decompressed version of the data
|
|
2292
|
+
*/
|
|
2293
|
+
function gunzipSync(data, opts) {
|
|
2294
|
+
var st = gzs(data);
|
|
2295
|
+
if (st + 8 > data.length)
|
|
2296
|
+
err(6, 'invalid gzip data');
|
|
2297
|
+
return inflt(data.subarray(st, -8), { i: 2 }, new u8(gzl(data)), opts);
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Expands Zlib data
|
|
2301
|
+
* @param data The data to decompress
|
|
2302
|
+
* @param opts The decompression options
|
|
2303
|
+
* @returns The decompressed version of the data
|
|
2304
|
+
*/
|
|
2305
|
+
function unzlibSync(data, opts) {
|
|
2306
|
+
return inflt(data.subarray(zls(data), -4), { i: 2 }, opts, opts);
|
|
2307
|
+
}
|
|
2308
|
+
// text decoder
|
|
2309
|
+
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
|
|
2310
|
+
// text decoder stream
|
|
2311
|
+
var tds = 0;
|
|
2312
|
+
try {
|
|
2313
|
+
td.decode(et, { stream: true });
|
|
2314
|
+
tds = 1;
|
|
2315
|
+
}
|
|
2316
|
+
catch (e) { }
|
|
2317
|
+
|
|
2318
|
+
/**
|
|
2319
|
+
* Implémentation simple du Bloom Filter pour SDK VIPros
|
|
2320
|
+
* Compatible avec le générateur PHP côté serveur
|
|
2321
|
+
*/
|
|
2322
|
+
class SimpleBloomFilter {
|
|
2323
|
+
constructor(compressedData, options = {}) {
|
|
2324
|
+
this.debugEnabled = Boolean(options.debug);
|
|
2325
|
+
try {
|
|
2326
|
+
// Décompresser les données gzip (simulation simple)
|
|
2327
|
+
this.bits = this.decompress(compressedData);
|
|
2328
|
+
this.size = this.bits.length * 8;
|
|
2329
|
+
this.k = 7; // Nombre de fonctions de hachage (même que côté serveur)
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
console.warn('[BloomFilter] Decompression failed, using raw data', error);
|
|
2332
|
+
this.bits = compressedData;
|
|
2333
|
+
this.size = this.bits.length * 8;
|
|
2334
|
+
this.k = 7;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* Teste si un EAN pourrait être présent dans le filtre
|
|
2340
|
+
* @param {string} ean - Code EAN à tester
|
|
2341
|
+
* @returns {boolean} true si l'EAN pourrait être présent, false s'il est définitivement absent
|
|
2342
|
+
*/
|
|
2343
|
+
test(ean) {
|
|
2344
|
+
if (!this.isValidEan(ean)) {
|
|
2345
|
+
this.debug('[BloomFilter] Invalid EAN format:', ean);
|
|
2346
|
+
return false
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
this.debug('[BloomFilter] Testing EAN:', ean, 'Size:', this.size, 'Bits length:', this.bits.length);
|
|
2350
|
+
|
|
2351
|
+
for (let i = 0; i < this.k; i++) {
|
|
2352
|
+
const fullHash = this.hash(ean + i);
|
|
2353
|
+
const hash = fullHash % this.size;
|
|
2354
|
+
const byte = Math.floor(hash / 8);
|
|
2355
|
+
const bit = hash % 8;
|
|
2356
|
+
const bitValue = !!(this.bits[byte] & (1 << bit));
|
|
2357
|
+
|
|
2358
|
+
this.debug(`[BloomFilter] Hash ${i}: ${fullHash} % ${this.size} = ${hash}, byte=${byte}, bit=${bit}, value=${bitValue}`);
|
|
2359
|
+
|
|
2360
|
+
if (!bitValue) {
|
|
2361
|
+
this.debug(`[BloomFilter] EAN ${ean} NOT found (hash ${i} failed)`);
|
|
2362
|
+
return false // Définitivement absent
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
this.debug(`[BloomFilter] EAN ${ean} FOUND (all ${this.k} hashes passed)`);
|
|
2366
|
+
return true // Peut-être présent
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* Fonction de hachage CRC32 compatible avec PHP
|
|
2371
|
+
* @param {string} str - Chaîne à hacher
|
|
2372
|
+
* @returns {number} Hash absolu
|
|
2373
|
+
*/
|
|
2374
|
+
hash(str) {
|
|
2375
|
+
// Implémentation CRC32 compatible avec PHP
|
|
2376
|
+
const crcTable = this.getCrc32Table();
|
|
2377
|
+
let crc = 0xFFFFFFFF;
|
|
2378
|
+
|
|
2379
|
+
for (let i = 0; i < str.length; i++) {
|
|
2380
|
+
const char = str.charCodeAt(i);
|
|
2381
|
+
crc = (crc >>> 8) ^ crcTable[(crc ^ char) & 0xFF];
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
return Math.abs((crc ^ 0xFFFFFFFF) >>> 0)
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* Génère la table CRC32
|
|
2389
|
+
* @returns {Uint32Array} Table CRC32
|
|
2390
|
+
*/
|
|
2391
|
+
getCrc32Table() {
|
|
2392
|
+
if (this.crc32Table) {
|
|
2393
|
+
return this.crc32Table
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// Utiliser une table CRC32 pré-calculée pour éviter les problèmes de stack
|
|
2397
|
+
this.crc32Table = new Uint32Array([
|
|
2398
|
+
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
|
|
2399
|
+
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
|
2400
|
+
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
|
|
2401
|
+
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
|
2402
|
+
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
|
|
2403
|
+
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
|
2404
|
+
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
|
2405
|
+
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
|
2406
|
+
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
|
|
2407
|
+
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
|
2408
|
+
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
|
|
2409
|
+
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
|
2410
|
+
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
|
|
2411
|
+
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
|
2412
|
+
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
|
|
2413
|
+
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
|
2414
|
+
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
|
|
2415
|
+
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
|
2416
|
+
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
|
|
2417
|
+
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
|
2418
|
+
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
|
2419
|
+
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
|
2420
|
+
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
|
|
2421
|
+
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
|
2422
|
+
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
|
|
2423
|
+
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
|
2424
|
+
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
|
|
2425
|
+
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
|
2426
|
+
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
|
|
2427
|
+
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
|
2428
|
+
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
|
|
2429
|
+
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
|
2430
|
+
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
|
|
2431
|
+
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
|
2432
|
+
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
|
2433
|
+
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
|
2434
|
+
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
|
|
2435
|
+
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
|
2436
|
+
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
|
|
2437
|
+
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
|
2438
|
+
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
|
|
2439
|
+
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
|
2440
|
+
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
|
2441
|
+
]);
|
|
2442
|
+
|
|
2443
|
+
return this.crc32Table
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/**
|
|
2447
|
+
* Valide le format d'un EAN
|
|
2448
|
+
* @param {string} ean - EAN à valider
|
|
2449
|
+
* @returns {boolean} true si valide
|
|
2450
|
+
*/
|
|
2451
|
+
isValidEan(ean) {
|
|
2452
|
+
return typeof ean === 'string' && /^\d{8}$|^\d{12}$|^\d{13}$/.test(ean)
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* Décompresse les données zlib/gzcompress
|
|
2457
|
+
* @param {Uint8Array} data - Données compressées
|
|
2458
|
+
* @returns {Uint8Array} Données décompressées
|
|
2459
|
+
*/
|
|
2460
|
+
decompress(data) {
|
|
2461
|
+
try {
|
|
2462
|
+
let buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2463
|
+
|
|
2464
|
+
if (buffer.length === 0) {
|
|
2465
|
+
return buffer
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
if (buffer[0] === 0x78) {
|
|
2469
|
+
this.debug('[BloomFilter] Detected zlib/gzcompress data, inflating');
|
|
2470
|
+
try {
|
|
2471
|
+
return unzlibSync(buffer)
|
|
2472
|
+
} catch (inflateError) {
|
|
2473
|
+
this.debug('[BloomFilter] unzlibSync failed, fallback to raw:', inflateError.message);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
|
2478
|
+
this.debug('[BloomFilter] Detected gzip data, inflating');
|
|
2479
|
+
try {
|
|
2480
|
+
return gunzipSync(buffer)
|
|
2481
|
+
} catch (gunzipError) {
|
|
2482
|
+
this.debug('[BloomFilter] gunzipSync failed, fallback to raw:', gunzipError.message);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
} catch (error) {
|
|
2486
|
+
this.debug('[BloomFilter] Decompression failed:', error.message);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
const fallbackBuffer = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
2490
|
+
this.debug('[BloomFilter] Using raw data, size:', fallbackBuffer.length);
|
|
2491
|
+
return fallbackBuffer
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/**
|
|
2495
|
+
* Retourne les statistiques du filtre
|
|
2496
|
+
* @returns {Object} Statistiques
|
|
2497
|
+
*/
|
|
2498
|
+
getStats() {
|
|
2499
|
+
return {
|
|
2500
|
+
size: this.size,
|
|
2501
|
+
bytes: this.bits.length,
|
|
2502
|
+
hashFunctions: this.k,
|
|
2503
|
+
loaded: true
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
debug(...args) {
|
|
2508
|
+
if (this.debugEnabled) {
|
|
2509
|
+
console.debug(...args);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
class ApiClient extends EventEmitter {
|
|
2515
|
+
constructor (config) {
|
|
2516
|
+
super();
|
|
2517
|
+
this.config = config;
|
|
2518
|
+
this.baseUrl = this.config.apiBaseUrl;
|
|
2519
|
+
this.defaultHeaders = this.buildDefaultHeaders();
|
|
2520
|
+
this.retryConfig = {
|
|
2521
|
+
maxRetries: 3,
|
|
2522
|
+
baseDelay: 1000,
|
|
2523
|
+
maxDelay: 10000
|
|
2524
|
+
};
|
|
2525
|
+
this.requestQueue = [];
|
|
2526
|
+
this.isProcessingQueue = false;
|
|
2527
|
+
this.rateLimitInfo = {
|
|
2528
|
+
remaining: null,
|
|
2529
|
+
resetTime: null,
|
|
2530
|
+
limit: null
|
|
2531
|
+
};
|
|
2532
|
+
// Bloom Filter pour optimisation invisible
|
|
2533
|
+
this.bloomFilter = null;
|
|
2534
|
+
this.bloomLoaded = false;
|
|
2535
|
+
this.bloomLoadPromise = null;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
buildDefaultHeaders () {
|
|
2539
|
+
const sdkVersion = this.config.version || '3.0.0-unified';
|
|
2540
|
+
const headers = {
|
|
2541
|
+
'Content-Type': 'application/json',
|
|
2542
|
+
'Accept': 'application/json',
|
|
2543
|
+
'User-Agent': `ViprosSDK/${sdkVersion} (JavaScript)`,
|
|
2544
|
+
'X-SDK-Version': sdkVersion,
|
|
2545
|
+
'X-SDK-Client': 'JavaScript'
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
if (this.config.apiKey) {
|
|
2549
|
+
headers['x-api-key'] = this.config.apiKey;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
2553
|
+
headers['Referer'] = window.location.href;
|
|
2554
|
+
headers['Origin'] = window.location.origin;
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
return headers
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
async getCatalogItems (filters = {}, options = {}) {
|
|
2561
|
+
this.debug('[ViprosSDK] getCatalogItems() called with filters:', filters, 'options:', options);
|
|
2562
|
+
|
|
2563
|
+
const params = new URLSearchParams();
|
|
2564
|
+
|
|
2565
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
2566
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
2567
|
+
params.append(key, value.toString());
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
|
|
2571
|
+
// Support pour la sync inline (nouveau en Phase 8)
|
|
2572
|
+
if (options.enableInlineSync) {
|
|
2573
|
+
params.append('sync', 'auto');
|
|
2574
|
+
|
|
2575
|
+
// Passer l'URL du produit pour la sync
|
|
2576
|
+
if (options.productUrl) {
|
|
2577
|
+
params.append('product_url', options.productUrl);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// Force sync si demandé
|
|
2581
|
+
if (options.forceSync) {
|
|
2582
|
+
params.append('force_sync', '1');
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// Activer debug mode pour obtenir les métadonnées de sync
|
|
2586
|
+
params.append('debug', 'true');
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// Utiliser l'endpoint SDK sécurisé
|
|
2590
|
+
const url = `${this.baseUrl}/sdk/catalog/items${params.toString() ? '?' + params.toString() : ''}`;
|
|
2591
|
+
|
|
2592
|
+
// Headers additionnels pour la sync inline
|
|
2593
|
+
const headers = {};
|
|
2594
|
+
if (options.enableInlineSync && options.productUrl) {
|
|
2595
|
+
headers['X-Product-Url'] = options.productUrl;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
return this.request('GET', url, null, { headers })
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Récupère un produit unique par son EAN (route optimisée)
|
|
2603
|
+
* Plus performante que getCatalogItems() car évite la pagination et le count ES
|
|
2604
|
+
*
|
|
2605
|
+
* @param {string} ean - Code EAN du produit (8-14 chiffres)
|
|
2606
|
+
* @param {Object} options - Options de requête
|
|
2607
|
+
* @param {boolean} options.enableInlineSync - Activer la sync inline
|
|
2608
|
+
* @param {string} options.productUrl - URL du produit pour la sync
|
|
2609
|
+
* @param {boolean} options.forceSync - Forcer la sync même si cooldown actif
|
|
2610
|
+
* @returns {Promise<Object|null>} Produit ou null si non trouvé
|
|
2611
|
+
*/
|
|
2612
|
+
async getCatalogItemByEan (ean, options = {}) {
|
|
2613
|
+
this.debug('[ViprosSDK] getCatalogItemByEan() called with ean:', ean, 'options:', options);
|
|
2614
|
+
|
|
2615
|
+
// Vérification Bloom Filter invisible
|
|
2616
|
+
const shouldSkip = await this.shouldSkipEan(ean);
|
|
2617
|
+
if (shouldSkip) {
|
|
2618
|
+
this.debug('[ViprosSDK] getCatalogItemByEan() - EAN not in bloom filter, skipping API call');
|
|
2619
|
+
return null
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// Construire les paramètres de requête
|
|
2623
|
+
const params = new URLSearchParams();
|
|
2624
|
+
|
|
2625
|
+
// Support pour la sync inline
|
|
2626
|
+
if (options.enableInlineSync) {
|
|
2627
|
+
params.append('sync', 'auto');
|
|
2628
|
+
|
|
2629
|
+
if (options.productUrl) {
|
|
2630
|
+
params.append('product_url', options.productUrl);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
if (options.forceSync) {
|
|
2634
|
+
params.append('force_sync', '1');
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
params.append('debug', 'true');
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// Utiliser l'endpoint SDK optimisé avec EAN dans le path
|
|
2641
|
+
const queryString = params.toString();
|
|
2642
|
+
const url = `${this.baseUrl}/sdk/catalog/items/${encodeURIComponent(ean)}${queryString ? '?' + queryString : ''}`;
|
|
2643
|
+
|
|
2644
|
+
// Headers additionnels pour la sync inline
|
|
2645
|
+
const headers = {};
|
|
2646
|
+
if (options.enableInlineSync && options.productUrl) {
|
|
2647
|
+
headers['X-Product-Url'] = options.productUrl;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
try {
|
|
2651
|
+
const response = await this.request('GET', url, null, { headers });
|
|
2652
|
+
|
|
2653
|
+
// Gérer le cas 404 (produit non trouvé)
|
|
2654
|
+
if (response.code === 404 || !response.item) {
|
|
2655
|
+
this.debug('[ViprosSDK] getCatalogItemByEan() - Product not found for EAN:', ean);
|
|
2656
|
+
return null
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
this.debug('[ViprosSDK] getCatalogItemByEan() - Product found:', response.item?.name);
|
|
2660
|
+
return response.item
|
|
2661
|
+
} catch (error) {
|
|
2662
|
+
this.debug('[ViprosSDK] getCatalogItemByEan() - Error:', error?.message || error);
|
|
2663
|
+
// Graceful degradation - return null instead of throwing
|
|
2664
|
+
return null
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
async updateProductUrls (updates) {
|
|
2669
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
2670
|
+
throw new Error('Updates must be a non-empty array')
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
const url = `${this.baseUrl}/catalog/stores/update_urls`;
|
|
2674
|
+
return this.request('POST', url, { updates })
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
async getProductUpdates () {
|
|
2678
|
+
const url = `${this.baseUrl}/catalog/stores/get_updates`;
|
|
2679
|
+
return this.request('GET', url)
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
async healthCheck () {
|
|
2683
|
+
const url = `${this.baseUrl}/sdk/health`;
|
|
2684
|
+
return this.request('GET', url)
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
async syncProduct (ean, productUrl, forceSync = false) {
|
|
2688
|
+
if (!ean || !productUrl) {
|
|
2689
|
+
throw new Error('EAN and product URL are required for synchronization')
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
const url = `${this.config.apiBaseUrl}/partners-stores/sync`;
|
|
2693
|
+
|
|
2694
|
+
const headers = {};
|
|
2695
|
+
|
|
2696
|
+
const payload = {
|
|
2697
|
+
ean: ean.toString(),
|
|
2698
|
+
url: productUrl
|
|
2699
|
+
};
|
|
2700
|
+
|
|
2701
|
+
// Ajouter le paramètre force_sync si nécessaire
|
|
2702
|
+
if (forceSync) {
|
|
2703
|
+
payload.force_sync = true;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
return this.request('POST', url, payload, { headers })
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/**
|
|
2710
|
+
* Méthode GET simple pour Web Components
|
|
2711
|
+
* @param {string} endpoint - Endpoint (ex: '/catalog/items')
|
|
2712
|
+
* @param {Object} params - Paramètres de requête
|
|
2713
|
+
* @param {Object} options - Options additionnelles (enableInlineSync, productUrl, forceSync)
|
|
2714
|
+
* @returns {Promise<Object>}
|
|
2715
|
+
*/
|
|
2716
|
+
async get(endpoint, params = {}, options = {}) {
|
|
2717
|
+
// Rediriger les appels SDK vers les endpoints optimisés
|
|
2718
|
+
if (endpoint === '/catalog/items') {
|
|
2719
|
+
// Si un seul EAN est demandé, utiliser la route optimisée
|
|
2720
|
+
if (params.ean && !params.ean.includes(',')) {
|
|
2721
|
+
this.debug('[ViprosSDK] get() method - using optimized single EAN route');
|
|
2722
|
+
const item = await this.getCatalogItemByEan(params.ean, options);
|
|
2723
|
+
|
|
2724
|
+
// Retourner au format compatible avec l'ancien format items[]
|
|
2725
|
+
if (item) {
|
|
2726
|
+
return {
|
|
2727
|
+
items: [item],
|
|
2728
|
+
code: 200
|
|
2729
|
+
}
|
|
2730
|
+
} else {
|
|
2731
|
+
return {
|
|
2732
|
+
items: [],
|
|
2733
|
+
code: 404,
|
|
2734
|
+
metadata: {
|
|
2735
|
+
ean: params.ean,
|
|
2736
|
+
optimizedRoute: true
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// Pour les autres cas (pas d'EAN ou plusieurs EANs), utiliser l'ancienne route
|
|
2743
|
+
// Vérification Bloom Filter invisible
|
|
2744
|
+
if (params.ean) {
|
|
2745
|
+
const shouldSkip = await this.shouldSkipEan(params.ean);
|
|
2746
|
+
this.debug(`[ViprosSDK] get() method - shouldSkip: ${shouldSkip}`);
|
|
2747
|
+
if (shouldSkip) {
|
|
2748
|
+
// Retourner réponse vide sans appel API, indiquer le court-circuit Bloom
|
|
2749
|
+
this.debug('[ViprosSDK] get() method - returning empty items, API call skipped (bloom filter)');
|
|
2750
|
+
return {
|
|
2751
|
+
items: [],
|
|
2752
|
+
metadata: {
|
|
2753
|
+
bloomFilter: {
|
|
2754
|
+
skipped: true,
|
|
2755
|
+
reason: 'NOT_IN_FILTER',
|
|
2756
|
+
ean: params.ean
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
this.debug('[ViprosSDK] get() method - calling getCatalogItems() with options:', options);
|
|
2764
|
+
return this.getCatalogItems(params, options)
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// Pour les autres endpoints, construire l'URL et faire la requête
|
|
2768
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
2769
|
+
const searchParams = new URLSearchParams();
|
|
2770
|
+
|
|
2771
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
2772
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
2773
|
+
searchParams.append(key, value.toString());
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
|
|
2777
|
+
const fullUrl = searchParams.toString() ? `${url}?${searchParams.toString()}` : url;
|
|
2778
|
+
return this.request('GET', fullUrl)
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
async request (method, url, data = null, options = {}) {
|
|
2782
|
+
return new Promise((resolve, reject) => {
|
|
2783
|
+
const requestConfig = {
|
|
2784
|
+
method,
|
|
2785
|
+
url,
|
|
2786
|
+
data,
|
|
2787
|
+
options: {
|
|
2788
|
+
retry: true,
|
|
2789
|
+
...options
|
|
2790
|
+
},
|
|
2791
|
+
resolve,
|
|
2792
|
+
reject,
|
|
2793
|
+
attempts: 0
|
|
2794
|
+
};
|
|
2795
|
+
|
|
2796
|
+
this.requestQueue.push(requestConfig);
|
|
2797
|
+
this.processQueue();
|
|
2798
|
+
})
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
async processQueue () {
|
|
2802
|
+
if (this.isProcessingQueue || this.requestQueue.length === 0) {
|
|
2803
|
+
return
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
this.isProcessingQueue = true;
|
|
2807
|
+
|
|
2808
|
+
while (this.requestQueue.length > 0) {
|
|
2809
|
+
const requestConfig = this.requestQueue.shift();
|
|
2810
|
+
|
|
2811
|
+
try {
|
|
2812
|
+
const result = await this.executeRequest(requestConfig);
|
|
2813
|
+
requestConfig.resolve(result);
|
|
2814
|
+
} catch (error) {
|
|
2815
|
+
if (requestConfig.options.retry && this.shouldRetry(error, requestConfig)) {
|
|
2816
|
+
await this.scheduleRetry(requestConfig);
|
|
2817
|
+
} else {
|
|
2818
|
+
requestConfig.reject(error);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
if (this.rateLimitInfo.remaining !== null && this.rateLimitInfo.remaining <= 1) {
|
|
2823
|
+
const delay = this.calculateRateLimitDelay();
|
|
2824
|
+
if (delay > 0) {
|
|
2825
|
+
await this.sleep(delay);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
this.isProcessingQueue = false;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
async executeRequest (requestConfig) {
|
|
2834
|
+
const { method, url, data, options } = requestConfig;
|
|
2835
|
+
requestConfig.attempts++;
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
const requestOptions = {
|
|
2839
|
+
method,
|
|
2840
|
+
headers: { ...this.defaultHeaders, ...options.headers },
|
|
2841
|
+
credentials: 'omit'
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
|
2845
|
+
requestOptions.body = JSON.stringify(data);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
if (options.timeout) {
|
|
2849
|
+
const controller = new AbortController();
|
|
2850
|
+
requestOptions.signal = controller.signal;
|
|
2851
|
+
setTimeout(() => controller.abort(), options.timeout);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
const startTime = Date.now();
|
|
2855
|
+
|
|
2856
|
+
try {
|
|
2857
|
+
const response = await fetch(url, requestOptions);
|
|
2858
|
+
const responseTime = Date.now() - startTime;
|
|
2859
|
+
|
|
2860
|
+
this.updateRateLimitInfo(response);
|
|
2861
|
+
|
|
2862
|
+
if (!response.ok) {
|
|
2863
|
+
const error = await this.handleErrorResponse(response);
|
|
2864
|
+
this.emit('error', error);
|
|
2865
|
+
throw error
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const result = await response.json();
|
|
2869
|
+
|
|
2870
|
+
this.emit('response', {
|
|
2871
|
+
url,
|
|
2872
|
+
method,
|
|
2873
|
+
status: response.status,
|
|
2874
|
+
responseTime,
|
|
2875
|
+
data: result
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
return result
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
|
|
2881
|
+
if (error.name === 'AbortError') {
|
|
2882
|
+
throw this.createNetworkError('TIMEOUT', `Request timeout after ${options.timeout}ms`, url, method)
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
2886
|
+
throw this.createNetworkError('NETWORK_ERROR', 'Network error - please check your connection', url, method, error)
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
throw error
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
async handleErrorResponse (response) {
|
|
2894
|
+
const contentType = response.headers.get('content-type');
|
|
2895
|
+
let errorData;
|
|
2896
|
+
|
|
2897
|
+
try {
|
|
2898
|
+
if (contentType && contentType.includes('application/json')) {
|
|
2899
|
+
errorData = await response.json();
|
|
2900
|
+
} else {
|
|
2901
|
+
errorData = { message: await response.text() };
|
|
2902
|
+
}
|
|
2903
|
+
} catch (parseError) {
|
|
2904
|
+
errorData = { message: `HTTP ${response.status}` };
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
const error = new Error(errorData.message || `HTTP ${response.status}`);
|
|
2908
|
+
error.status = response.status;
|
|
2909
|
+
error.code = errorData.code || response.status;
|
|
2910
|
+
error.url = response.url;
|
|
2911
|
+
error.data = errorData;
|
|
2912
|
+
|
|
2913
|
+
switch (response.status) {
|
|
2914
|
+
case 400:
|
|
2915
|
+
error.type = 'VALIDATION_ERROR';
|
|
2916
|
+
break
|
|
2917
|
+
case 401:
|
|
2918
|
+
error.type = 'AUTHENTICATION_ERROR';
|
|
2919
|
+
break
|
|
2920
|
+
case 403:
|
|
2921
|
+
error.type = 'AUTHORIZATION_ERROR';
|
|
2922
|
+
break
|
|
2923
|
+
case 404:
|
|
2924
|
+
error.type = 'NOT_FOUND';
|
|
2925
|
+
break
|
|
2926
|
+
case 429:
|
|
2927
|
+
error.type = 'RATE_LIMIT_ERROR';
|
|
2928
|
+
error.retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
|
|
2929
|
+
break
|
|
2930
|
+
case 500:
|
|
2931
|
+
error.type = 'SERVER_ERROR';
|
|
2932
|
+
break
|
|
2933
|
+
case 502:
|
|
2934
|
+
case 503:
|
|
2935
|
+
case 504:
|
|
2936
|
+
error.type = 'SERVICE_UNAVAILABLE';
|
|
2937
|
+
break
|
|
2938
|
+
default:
|
|
2939
|
+
error.type = 'UNKNOWN_ERROR';
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
return error
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
shouldRetry (error, requestConfig) {
|
|
2946
|
+
if (requestConfig.attempts >= this.retryConfig.maxRetries) {
|
|
2947
|
+
return false
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
if (!requestConfig.options.retry) {
|
|
2951
|
+
return false
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
// ❌ NE PAS retry les erreurs rate limit - les faire remonter immédiatement pour affichage
|
|
2955
|
+
if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
|
|
2956
|
+
return false
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
const retryableStatuses = [500, 502, 503, 504];
|
|
2960
|
+
const retryableTypes = ['SERVER_ERROR', 'SERVICE_UNAVAILABLE', 'TIMEOUT', 'NETWORK_ERROR'];
|
|
2961
|
+
|
|
2962
|
+
return retryableStatuses.includes(error.status) ||
|
|
2963
|
+
retryableTypes.includes(error.type) ||
|
|
2964
|
+
retryableTypes.includes(error.code)
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
async scheduleRetry (requestConfig) {
|
|
2968
|
+
const delay = this.calculateRetryDelay(requestConfig);
|
|
2969
|
+
|
|
2970
|
+
await this.sleep(delay);
|
|
2971
|
+
this.requestQueue.unshift(requestConfig);
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
calculateRetryDelay (requestConfig) {
|
|
2975
|
+
const baseDelay = this.retryConfig.baseDelay;
|
|
2976
|
+
const exponentialDelay = baseDelay * Math.pow(2, requestConfig.attempts - 1);
|
|
2977
|
+
const jitter = Math.random() * 1000;
|
|
2978
|
+
|
|
2979
|
+
return Math.min(exponentialDelay + jitter, this.retryConfig.maxDelay)
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
calculateRateLimitDelay () {
|
|
2983
|
+
if (!this.rateLimitInfo.resetTime) {
|
|
2984
|
+
return 0
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
const now = Date.now();
|
|
2988
|
+
const resetTime = this.rateLimitInfo.resetTime * 1000;
|
|
2989
|
+
|
|
2990
|
+
return Math.max(0, resetTime - now + 1000)
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
updateRateLimitInfo (response) {
|
|
2994
|
+
const limit = response.headers.get('X-RateLimit-Limit');
|
|
2995
|
+
const remaining = response.headers.get('X-RateLimit-Remaining');
|
|
2996
|
+
const reset = response.headers.get('X-RateLimit-Reset');
|
|
2997
|
+
|
|
2998
|
+
if (limit) this.rateLimitInfo.limit = parseInt(limit);
|
|
2999
|
+
if (remaining) this.rateLimitInfo.remaining = parseInt(remaining);
|
|
3000
|
+
if (reset) this.rateLimitInfo.resetTime = parseInt(reset);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
sleep (ms) {
|
|
3004
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
getRateLimitInfo () {
|
|
3008
|
+
return { ...this.rateLimitInfo }
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
getQueueStatus () {
|
|
3012
|
+
return {
|
|
3013
|
+
queueLength: this.requestQueue.length,
|
|
3014
|
+
isProcessing: this.isProcessingQueue
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
clearQueue () {
|
|
3019
|
+
this.requestQueue.forEach(req => {
|
|
3020
|
+
req.reject(new Error('Request queue cleared'));
|
|
3021
|
+
});
|
|
3022
|
+
this.requestQueue = [];
|
|
3023
|
+
this.isProcessingQueue = false;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
createNetworkError (code, message, url, method, originalError = null) {
|
|
3027
|
+
const error = new Error(message);
|
|
3028
|
+
error.code = code;
|
|
3029
|
+
error.url = url;
|
|
3030
|
+
error.method = method;
|
|
3031
|
+
if (originalError) {
|
|
3032
|
+
error.originalError = originalError;
|
|
3033
|
+
}
|
|
3034
|
+
return error
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
destroy () {
|
|
3038
|
+
this.clearQueue();
|
|
3039
|
+
this.removeAllListeners();
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// Méthodes Bloom Filter pour optimisation invisible
|
|
3043
|
+
async shouldSkipEan(ean) {
|
|
3044
|
+
try {
|
|
3045
|
+
this.debug('[ViprosSDK] Bloom filter check for EAN:', ean);
|
|
3046
|
+
|
|
3047
|
+
await this.loadBloomFilter();
|
|
3048
|
+
|
|
3049
|
+
if (!this.bloomFilter) {
|
|
3050
|
+
this.debug('[ViprosSDK] No bloom filter available, allowing API call');
|
|
3051
|
+
return false
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const isPresent = this.bloomFilter.test(ean);
|
|
3055
|
+
const shouldSkip = !isPresent;
|
|
3056
|
+
this.debug(`[ViprosSDK] Bloom filter test for ${ean}: ${isPresent}, shouldSkip: ${shouldSkip}`);
|
|
3057
|
+
|
|
3058
|
+
return shouldSkip
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
this.debug('[ViprosSDK] Bloom filter error:', error?.message || error);
|
|
3061
|
+
return false
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
async loadBloomFilter() {
|
|
3066
|
+
if (this.bloomLoaded) {
|
|
3067
|
+
return this.bloomFilter
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
if (this.bloomLoadPromise) {
|
|
3071
|
+
return this.bloomLoadPromise
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
this.bloomLoadPromise = (async () => {
|
|
3075
|
+
try {
|
|
3076
|
+
const storage = this.getStorage();
|
|
3077
|
+
if (storage) {
|
|
3078
|
+
const cached = storage.getItem('vipros_bloom');
|
|
3079
|
+
const cacheTime = storage.getItem('vipros_bloom_time');
|
|
3080
|
+
|
|
3081
|
+
if (cached && cacheTime && (Date.now() - parseInt(cacheTime, 10)) < 86400000) {
|
|
3082
|
+
this.bloomFilter = new SimpleBloomFilter(this.base64ToUint8Array(cached), { debug: this.isDebugEnabled() });
|
|
3083
|
+
this.debug('[ViprosSDK] Bloom filter loaded from cache');
|
|
3084
|
+
this.bloomLoaded = true;
|
|
3085
|
+
return this.bloomFilter
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
const response = await fetch(`${this.baseUrl}/sdk/bloom`, {
|
|
3090
|
+
headers: {
|
|
3091
|
+
'Accept': 'application/octet-stream',
|
|
3092
|
+
'x-api-key': this.config.apiKey
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
|
|
3096
|
+
if (!response.ok) {
|
|
3097
|
+
this.debug('[ViprosSDK] Unable to download bloom filter, status:', response.status);
|
|
3098
|
+
this.bloomFilter = null;
|
|
3099
|
+
this.bloomLoaded = true;
|
|
3100
|
+
return this.bloomFilter
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const buffer = await response.arrayBuffer();
|
|
3104
|
+
const data = new Uint8Array(buffer);
|
|
3105
|
+
|
|
3106
|
+
this.bloomFilter = new SimpleBloomFilter(data, { debug: this.isDebugEnabled() });
|
|
3107
|
+
|
|
3108
|
+
if (storage) {
|
|
3109
|
+
storage.setItem('vipros_bloom', this.uint8ArrayToBase64(data));
|
|
3110
|
+
storage.setItem('vipros_bloom_time', Date.now().toString());
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
this.debug('[ViprosSDK] Bloom filter downloaded and cached');
|
|
3114
|
+
this.bloomLoaded = true;
|
|
3115
|
+
return this.bloomFilter
|
|
3116
|
+
} catch (error) {
|
|
3117
|
+
this.debug('[ViprosSDK] Bloom filter unavailable:', error?.message || error);
|
|
3118
|
+
this.bloomFilter = null;
|
|
3119
|
+
this.bloomLoaded = true;
|
|
3120
|
+
return this.bloomFilter
|
|
3121
|
+
} finally {
|
|
3122
|
+
this.bloomLoadPromise = null;
|
|
3123
|
+
}
|
|
3124
|
+
})();
|
|
3125
|
+
|
|
3126
|
+
return this.bloomLoadPromise
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
uint8ArrayToBase64(uint8Array) {
|
|
3130
|
+
if (typeof globalThis !== 'undefined' && globalThis.Buffer) {
|
|
3131
|
+
return globalThis.Buffer.from(uint8Array).toString('base64')
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
let binary = '';
|
|
3135
|
+
const chunkSize = 0x8000;
|
|
3136
|
+
|
|
3137
|
+
for (let i = 0; i < uint8Array.length; i += chunkSize) {
|
|
3138
|
+
const chunk = uint8Array.subarray(i, i + chunkSize);
|
|
3139
|
+
let chunkText = '';
|
|
3140
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
3141
|
+
chunkText += String.fromCharCode(chunk[j]);
|
|
3142
|
+
}
|
|
3143
|
+
binary += chunkText;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
return btoa(binary)
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
base64ToUint8Array(base64) {
|
|
3150
|
+
if (typeof globalThis !== 'undefined' && globalThis.Buffer) {
|
|
3151
|
+
return new Uint8Array(globalThis.Buffer.from(base64, 'base64'))
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
const binary = atob(base64);
|
|
3155
|
+
const length = binary.length;
|
|
3156
|
+
const bytes = new Uint8Array(length);
|
|
3157
|
+
|
|
3158
|
+
for (let i = 0; i < length; i++) {
|
|
3159
|
+
bytes[i] = binary.charCodeAt(i);
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
return bytes
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
getStorage () {
|
|
3166
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
3167
|
+
return null
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
try {
|
|
3171
|
+
// Accessing localStorage can throw in some browsers (privacy mode)
|
|
3172
|
+
const testKey = '__vipros_storage_test__';
|
|
3173
|
+
window.localStorage.setItem(testKey, '1');
|
|
3174
|
+
window.localStorage.removeItem(testKey);
|
|
3175
|
+
return window.localStorage
|
|
3176
|
+
} catch (error) {
|
|
3177
|
+
this.debug('[ViprosSDK] localStorage unavailable:', error?.message || error);
|
|
3178
|
+
return null
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
debug(...args) {
|
|
3183
|
+
if (this.isDebugEnabled()) {
|
|
3184
|
+
console.debug(...args);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
isDebugEnabled () {
|
|
3189
|
+
return Boolean(this.config && this.config.debug)
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
var ApiClient$1 = /*#__PURE__*/Object.freeze({
|
|
3194
|
+
__proto__: null,
|
|
3195
|
+
default: ApiClient
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
/**
|
|
3199
|
+
* CacheService Simplifié - Cache Mémoire Uniquement
|
|
3200
|
+
*
|
|
3201
|
+
* Version optimisée qui utilise uniquement la mémoire.
|
|
3202
|
+
* La persistance est gérée côté serveur avec cooldown 24h.
|
|
3203
|
+
*/
|
|
3204
|
+
class CacheService extends EventEmitter {
|
|
3205
|
+
constructor (cacheTimeout = 300000) {
|
|
3206
|
+
super();
|
|
3207
|
+
// Validation: s'assurer que cacheTimeout est un nombre
|
|
3208
|
+
if (typeof cacheTimeout !== 'number' || isNaN(cacheTimeout) || cacheTimeout < 0) {
|
|
3209
|
+
console.warn('[CacheService] Invalid cacheTimeout, using default 300000ms');
|
|
3210
|
+
cacheTimeout = 300000;
|
|
3211
|
+
}
|
|
3212
|
+
this.cacheTimeout = cacheTimeout;
|
|
3213
|
+
this.memoryCache = new Map();
|
|
3214
|
+
|
|
3215
|
+
this.setupCleanupInterval();
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
get (key, useStorage = true) {
|
|
3219
|
+
const fullKey = this.buildKey(key);
|
|
3220
|
+
|
|
3221
|
+
let cached = this.memoryCache.get(fullKey);
|
|
3222
|
+
if (cached && this.isValidCacheEntry(cached)) {
|
|
3223
|
+
cached.hits = (cached.hits || 0) + 1;
|
|
3224
|
+
cached.lastAccessTime = Date.now();
|
|
3225
|
+
this.emit('hit', { key, source: 'memory' });
|
|
3226
|
+
return cached.data
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
this.emit('miss', { key });
|
|
3230
|
+
return null
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
set (key, data, options = {}) {
|
|
3234
|
+
const fullKey = this.buildKey(key);
|
|
3235
|
+
const entry = this.createCacheEntry(data, options);
|
|
3236
|
+
|
|
3237
|
+
this.memoryCache.set(fullKey, entry);
|
|
3238
|
+
|
|
3239
|
+
this.emit('set', { key, size: this.estimateSize(data) });
|
|
3240
|
+
this.enforceCacheLimits();
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
createCacheEntry (data, options = {}) {
|
|
3244
|
+
const now = Date.now();
|
|
3245
|
+
const ttl = options.ttl || this.cacheTimeout;
|
|
3246
|
+
|
|
3247
|
+
return {
|
|
3248
|
+
data,
|
|
3249
|
+
timestamp: now,
|
|
3250
|
+
expires: ttl > 0 ? now + ttl : null,
|
|
3251
|
+
hits: 0,
|
|
3252
|
+
lastAccessTime: now,
|
|
3253
|
+
size: this.estimateSize(data),
|
|
3254
|
+
tags: options.tags || [],
|
|
3255
|
+
version: options.version || '3.0.0'
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
isValidCacheEntry (entry) {
|
|
3260
|
+
if (!entry || typeof entry !== 'object') {
|
|
3261
|
+
return false
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
if (!entry.timestamp || !entry.data) {
|
|
3265
|
+
return false
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
if (entry.expires && Date.now() > entry.expires) {
|
|
3269
|
+
return false
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
return true
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
estimateSize (data) {
|
|
3276
|
+
try {
|
|
3277
|
+
return JSON.stringify(data).length
|
|
3278
|
+
} catch (error) {
|
|
3279
|
+
return 0
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
buildKey (key) {
|
|
3284
|
+
return `vipros_sdk_${key}`
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
has (key) {
|
|
3288
|
+
const fullKey = this.buildKey(key);
|
|
3289
|
+
|
|
3290
|
+
if (this.memoryCache.has(fullKey)) {
|
|
3291
|
+
const entry = this.memoryCache.get(fullKey);
|
|
3292
|
+
return this.isValidCacheEntry(entry)
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
return false
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
delete (key) {
|
|
3299
|
+
const fullKey = this.buildKey(key);
|
|
3300
|
+
const deleted = this.memoryCache.delete(fullKey);
|
|
3301
|
+
|
|
3302
|
+
if (deleted) {
|
|
3303
|
+
this.emit('delete', { key });
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
return deleted
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
clear () {
|
|
3310
|
+
const memorySize = this.memoryCache.size;
|
|
3311
|
+
this.memoryCache.clear();
|
|
3312
|
+
|
|
3313
|
+
this.emit('clear', { memorySize });
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
size () {
|
|
3317
|
+
return this.memoryCache.size
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
getStats () {
|
|
3321
|
+
const memoryStats = {
|
|
3322
|
+
entries: this.memoryCache.size,
|
|
3323
|
+
totalSize: 0,
|
|
3324
|
+
avgSize: 0
|
|
3325
|
+
};
|
|
3326
|
+
|
|
3327
|
+
for (const [, entry] of this.memoryCache) {
|
|
3328
|
+
memoryStats.totalSize += entry.size || 0;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
if (memoryStats.entries > 0) {
|
|
3332
|
+
memoryStats.avgSize = Math.round(memoryStats.totalSize / memoryStats.entries);
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
return { memory: memoryStats }
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
enforceCacheLimits () {
|
|
3339
|
+
const maxMemoryEntries = 100;
|
|
3340
|
+
const maxMemorySize = 10 * 1024 * 1024; // 10MB
|
|
3341
|
+
|
|
3342
|
+
if (this.memoryCache.size > maxMemoryEntries) {
|
|
3343
|
+
this.evictLeastRecentlyUsed(this.memoryCache.size - maxMemoryEntries);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
let totalSize = 0;
|
|
3347
|
+
for (const [, entry] of this.memoryCache) {
|
|
3348
|
+
totalSize += entry.size || 0;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
if (totalSize > maxMemorySize) {
|
|
3352
|
+
this.evictLargestEntries(totalSize - maxMemorySize);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
evictLeastRecentlyUsed (count) {
|
|
3357
|
+
const entries = Array.from(this.memoryCache.entries())
|
|
3358
|
+
.map(([key, entry]) => ({ key, lastAccess: entry.lastAccessTime || 0 }))
|
|
3359
|
+
.sort((a, b) => a.lastAccess - b.lastAccess)
|
|
3360
|
+
.slice(0, count);
|
|
3361
|
+
|
|
3362
|
+
entries.forEach(({ key }) => {
|
|
3363
|
+
this.memoryCache.delete(key);
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
this.emit('lru_eviction', { count });
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
evictLargestEntries (targetSize) {
|
|
3370
|
+
const entries = Array.from(this.memoryCache.entries())
|
|
3371
|
+
.map(([key, entry]) => ({ key, entry }))
|
|
3372
|
+
.sort((a, b) => (b.entry.size || 0) - (a.entry.size || 0));
|
|
3373
|
+
|
|
3374
|
+
let freedSize = 0;
|
|
3375
|
+
let evicted = 0;
|
|
3376
|
+
|
|
3377
|
+
for (const { key, entry } of entries) {
|
|
3378
|
+
if (freedSize >= targetSize) {
|
|
3379
|
+
break
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
this.memoryCache.delete(key);
|
|
3383
|
+
freedSize += entry.size || 0;
|
|
3384
|
+
evicted++;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
this.emit('size_eviction', { evicted, freedSize });
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
setupCleanupInterval () {
|
|
3391
|
+
if (typeof window !== 'undefined') {
|
|
3392
|
+
setInterval(() => {
|
|
3393
|
+
this.cleanupExpiredEntries();
|
|
3394
|
+
}, 60000);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
cleanupExpiredEntries () {
|
|
3399
|
+
let cleaned = 0;
|
|
3400
|
+
const now = Date.now();
|
|
3401
|
+
|
|
3402
|
+
for (const [key, entry] of this.memoryCache) {
|
|
3403
|
+
if (entry.expires && now > entry.expires) {
|
|
3404
|
+
this.memoryCache.delete(key);
|
|
3405
|
+
cleaned++;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
if (cleaned > 0) {
|
|
3410
|
+
this.emit('cleanup', { cleaned });
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
destroy () {
|
|
3415
|
+
this.clear();
|
|
3416
|
+
this.removeAllListeners();
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
/**
|
|
3421
|
+
* Service de synchronisation EAN avec l'API VIPros (version simplifiée)
|
|
3422
|
+
*
|
|
3423
|
+
* La logique de cooldown est maintenant gérée côté serveur Laravel.
|
|
3424
|
+
* Ce service se contente d'appeler l'API et de gérer les réponses.
|
|
3425
|
+
*/
|
|
3426
|
+
class EanSyncService extends EventEmitter {
|
|
3427
|
+
constructor (apiClient, cacheService, config = {}) {
|
|
3428
|
+
super();
|
|
3429
|
+
this.apiClient = apiClient;
|
|
3430
|
+
this.cacheService = cacheService;
|
|
3431
|
+
|
|
3432
|
+
// Configuration simplifiée
|
|
3433
|
+
this.config = {
|
|
3434
|
+
debug: config.debug || false,
|
|
3435
|
+
retryDelayMs: 5000, // 5 secondes pour retry réseau
|
|
3436
|
+
maxRetries: 2, // Retry limité aux erreurs réseau uniquement
|
|
3437
|
+
...config
|
|
3438
|
+
};
|
|
3439
|
+
|
|
3440
|
+
// Statistiques simplifiées
|
|
3441
|
+
this.stats = {
|
|
3442
|
+
attempted: 0,
|
|
3443
|
+
successful: 0,
|
|
3444
|
+
failed: 0,
|
|
3445
|
+
skipped: 0, // Skipped par le serveur (cooldown 202)
|
|
3446
|
+
rateLimited: 0,
|
|
3447
|
+
errors: []
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
/**
|
|
3452
|
+
* Lance une synchronisation en arrière-plan (fire & forget)
|
|
3453
|
+
* Version simplifiée - délègue la logique de cooldown au serveur
|
|
3454
|
+
* @param {string} ean - Code EAN du produit
|
|
3455
|
+
* @param {string} productUrl - URL de la page produit (généralement window.location.href)
|
|
3456
|
+
* @returns {void} - Ne retourne rien (fire & forget)
|
|
3457
|
+
*/
|
|
3458
|
+
syncProductInBackground (ean, productUrl) {
|
|
3459
|
+
if (!ean || !productUrl) {
|
|
3460
|
+
if (this.config.debug) {
|
|
3461
|
+
console.debug('[ViprosSDK] Sync skipped: missing EAN or URL');
|
|
3462
|
+
}
|
|
3463
|
+
return
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// Exécution simple - Fire & forget
|
|
3467
|
+
this.performSync(ean, productUrl).catch(error => {
|
|
3468
|
+
// Log silencieux en mode debug uniquement
|
|
3469
|
+
if (this.config.debug) {
|
|
3470
|
+
console.debug(`[ViprosSDK] Sync error for ${ean}:`, error.message);
|
|
3471
|
+
}
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
/**
|
|
3476
|
+
* Effectue la synchronisation réelle avec l'API (version simplifiée)
|
|
3477
|
+
* @private
|
|
3478
|
+
*/
|
|
3479
|
+
async performSync (ean, productUrl, retryCount = 0) {
|
|
3480
|
+
// Démarrer la synchronisation
|
|
3481
|
+
this.stats.attempted++;
|
|
3482
|
+
|
|
3483
|
+
try {
|
|
3484
|
+
// Appel API de synchronisation - délègue toute la logique au serveur
|
|
3485
|
+
const result = await this.apiClient.syncProduct(ean, productUrl);
|
|
3486
|
+
|
|
3487
|
+
// Traiter la réponse en fonction du code de statut
|
|
3488
|
+
if (result.code === 200) {
|
|
3489
|
+
// Succès - synchronisation effectuée
|
|
3490
|
+
this.stats.successful++;
|
|
3491
|
+
this.emit('syncSuccess', {
|
|
3492
|
+
ean,
|
|
3493
|
+
productUrl,
|
|
3494
|
+
result,
|
|
3495
|
+
timestamp: new Date()
|
|
3496
|
+
});
|
|
3497
|
+
|
|
3498
|
+
if (this.config.debug) {
|
|
3499
|
+
console.debug(`[ViprosSDK] Sync successful for ${ean}`);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
return { status: 'success', result }
|
|
3503
|
+
|
|
3504
|
+
} else if (result.code === 202) {
|
|
3505
|
+
// Cooldown actif côté serveur
|
|
3506
|
+
this.stats.skipped++;
|
|
3507
|
+
this.emit('syncSkipped', {
|
|
3508
|
+
ean,
|
|
3509
|
+
reason: 'server_cooldown',
|
|
3510
|
+
remaining_hours: result.remaining_hours,
|
|
3511
|
+
next_sync_at: result.next_sync_at
|
|
3512
|
+
});
|
|
3513
|
+
|
|
3514
|
+
if (this.config.debug) {
|
|
3515
|
+
console.debug(`[ViprosSDK] Sync skipped for ${ean}: server cooldown (${result.remaining_hours}h remaining)`);
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
return { status: 'skipped', reason: 'server_cooldown' }
|
|
3519
|
+
|
|
3520
|
+
} else {
|
|
3521
|
+
// Autre code de statut - traiter comme erreur
|
|
3522
|
+
throw new Error(`API returned status ${result.code}: ${result.message || 'Unknown error'}`)
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
} catch (error) {
|
|
3526
|
+
return this.handleSyncError(ean, productUrl, error, retryCount)
|
|
3527
|
+
} finally {
|
|
3528
|
+
// Cleanup if needed
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
|
|
3533
|
+
/**
|
|
3534
|
+
* Gère les erreurs de synchronisation (version simplifiée)
|
|
3535
|
+
* @private
|
|
3536
|
+
*/
|
|
3537
|
+
handleSyncError (ean, productUrl, error, retryCount) {
|
|
3538
|
+
this.stats.failed++;
|
|
3539
|
+
|
|
3540
|
+
// Analyser le type d'erreur
|
|
3541
|
+
const errorType = this.categorizeError(error);
|
|
3542
|
+
|
|
3543
|
+
// Gestion spéciale du rate limiting
|
|
3544
|
+
if (errorType === 'RATE_LIMIT') {
|
|
3545
|
+
this.stats.rateLimited++;
|
|
3546
|
+
this.emit('syncRateLimited', {
|
|
3547
|
+
ean,
|
|
3548
|
+
productUrl,
|
|
3549
|
+
error,
|
|
3550
|
+
retryAfter: error.retryAfter || 60
|
|
3551
|
+
});
|
|
3552
|
+
|
|
3553
|
+
if (this.config.debug) {
|
|
3554
|
+
console.debug(`[ViprosSDK] Sync rate limited for ${ean}`);
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
return { status: 'rate_limited', error: error.message }
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// Pour les erreurs réseau, on peut retry une fois
|
|
3561
|
+
if (errorType === 'NETWORK_ERROR' && retryCount < this.config.maxRetries) {
|
|
3562
|
+
if (this.config.debug) {
|
|
3563
|
+
console.debug(`[ViprosSDK] Retrying sync for ${ean} (attempt ${retryCount + 1})`);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// Retry après délai
|
|
3567
|
+
setTimeout(() => {
|
|
3568
|
+
this.performSync(ean, productUrl, retryCount + 1).catch(() => {
|
|
3569
|
+
// Ignore retry errors - already handled
|
|
3570
|
+
});
|
|
3571
|
+
}, this.config.retryDelayMs);
|
|
3572
|
+
|
|
3573
|
+
return { status: 'retrying', attempt: retryCount + 1 }
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// Enregistrer l'erreur
|
|
3577
|
+
this.logError(ean, error, errorType);
|
|
3578
|
+
|
|
3579
|
+
// Émettre l'événement d'erreur
|
|
3580
|
+
this.emit('syncError', {
|
|
3581
|
+
ean,
|
|
3582
|
+
productUrl,
|
|
3583
|
+
error,
|
|
3584
|
+
errorType,
|
|
3585
|
+
retryCount
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3588
|
+
// Log pour debug
|
|
3589
|
+
if (this.config.debug) {
|
|
3590
|
+
console.error(`[ViprosSDK] Sync failed for ${ean}:`, error);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
return { status: 'error', error: error.message, errorType }
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
/**
|
|
3597
|
+
* Catégorise une erreur pour déterminer l'action appropriée
|
|
3598
|
+
* @private
|
|
3599
|
+
*/
|
|
3600
|
+
categorizeError (error) {
|
|
3601
|
+
if (error.status === 429 || error.type === 'RATE_LIMIT_ERROR') {
|
|
3602
|
+
return 'RATE_LIMIT'
|
|
3603
|
+
}
|
|
3604
|
+
if (error.status >= 500 || error.type === 'SERVER_ERROR') {
|
|
3605
|
+
return 'SERVER_ERROR'
|
|
3606
|
+
}
|
|
3607
|
+
if (error.code === 'NETWORK_ERROR' || error.name === 'NetworkError') {
|
|
3608
|
+
return 'NETWORK_ERROR'
|
|
3609
|
+
}
|
|
3610
|
+
if (error.status === 400 || error.type === 'VALIDATION_ERROR') {
|
|
3611
|
+
return 'VALIDATION_ERROR'
|
|
3612
|
+
}
|
|
3613
|
+
return 'UNKNOWN'
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
|
|
3617
|
+
/**
|
|
3618
|
+
* Enregistre une erreur pour analyse
|
|
3619
|
+
* @private
|
|
3620
|
+
*/
|
|
3621
|
+
logError (ean, error, type) {
|
|
3622
|
+
this.stats.errors.push({
|
|
3623
|
+
ean,
|
|
3624
|
+
error: error.message,
|
|
3625
|
+
type,
|
|
3626
|
+
timestamp: Date.now()
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
// Limiter la taille du log d'erreurs
|
|
3630
|
+
if (this.stats.errors.length > 100) {
|
|
3631
|
+
this.stats.errors = this.stats.errors.slice(-50);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
/**
|
|
3636
|
+
* Retourne les statistiques de synchronisation (simplifiées)
|
|
3637
|
+
*/
|
|
3638
|
+
getSyncStats () {
|
|
3639
|
+
return {
|
|
3640
|
+
...this.stats,
|
|
3641
|
+
successRate: this.stats.attempted > 0
|
|
3642
|
+
? (this.stats.successful / this.stats.attempted * 100).toFixed(2) + '%'
|
|
3643
|
+
: '0%'
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
/**
|
|
3648
|
+
* Réinitialise les statistiques de synchronisation
|
|
3649
|
+
*/
|
|
3650
|
+
clearSyncStats () {
|
|
3651
|
+
this.stats = {
|
|
3652
|
+
attempted: 0,
|
|
3653
|
+
successful: 0,
|
|
3654
|
+
failed: 0,
|
|
3655
|
+
skipped: 0,
|
|
3656
|
+
rateLimited: 0,
|
|
3657
|
+
errors: []
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
/**
|
|
3662
|
+
* Force une synchronisation immédiate (version simplifiée)
|
|
3663
|
+
* Utilise le paramètre force_sync côté serveur
|
|
3664
|
+
*/
|
|
3665
|
+
async forceSyncNow (ean, productUrl) {
|
|
3666
|
+
// Effectuer la sync avec force_sync = true
|
|
3667
|
+
return this.performSyncWithForce(ean, productUrl)
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
/**
|
|
3671
|
+
* Effectue une synchronisation forcée
|
|
3672
|
+
* @private
|
|
3673
|
+
*/
|
|
3674
|
+
async performSyncWithForce(ean, productUrl) {
|
|
3675
|
+
this.stats.attempted++;
|
|
3676
|
+
|
|
3677
|
+
try {
|
|
3678
|
+
// Appel API avec force_sync = true
|
|
3679
|
+
const result = await this.apiClient.syncProduct(ean, productUrl, true);
|
|
3680
|
+
|
|
3681
|
+
if (result.code === 200) {
|
|
3682
|
+
this.stats.successful++;
|
|
3683
|
+
this.emit('syncSuccess', {
|
|
3684
|
+
ean,
|
|
3685
|
+
productUrl,
|
|
3686
|
+
result,
|
|
3687
|
+
forced: true,
|
|
3688
|
+
timestamp: new Date()
|
|
3689
|
+
});
|
|
3690
|
+
|
|
3691
|
+
return { status: 'success', result, forced: true }
|
|
3692
|
+
} else {
|
|
3693
|
+
throw new Error(`API returned status ${result.code}: ${result.message || 'Unknown error'}`)
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
} catch (error) {
|
|
3697
|
+
this.stats.failed++;
|
|
3698
|
+
this.logError(ean, error, 'forced_sync');
|
|
3699
|
+
|
|
3700
|
+
this.emit('syncError', {
|
|
3701
|
+
ean,
|
|
3702
|
+
productUrl,
|
|
3703
|
+
error,
|
|
3704
|
+
forced: true
|
|
3705
|
+
});
|
|
3706
|
+
|
|
3707
|
+
return { status: 'error', error: error.message, forced: true }
|
|
3708
|
+
} finally {
|
|
3709
|
+
// Cleanup if needed
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
/**
|
|
3714
|
+
* Nettoie et détruit le service (version simplifiée)
|
|
3715
|
+
*/
|
|
3716
|
+
destroy () {
|
|
3717
|
+
// Nettoyer les statistiques
|
|
3718
|
+
this.clearSyncStats();
|
|
3719
|
+
|
|
3720
|
+
// Retirer tous les listeners
|
|
3721
|
+
this.removeAllListeners();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
/**
|
|
3726
|
+
* VIPros SDK Unifié
|
|
3727
|
+
*
|
|
3728
|
+
* SDK moderne basé sur les Web Components avec rétrocompatibilité pour l'ancien système.
|
|
3729
|
+
* Combine les meilleures fonctionnalités des deux approches précédentes.
|
|
3730
|
+
*
|
|
3731
|
+
* Utilisation moderne (recommandée):
|
|
3732
|
+
* <vipros-offer-card ean="4007123684731"></vipros-offer-card>
|
|
3733
|
+
*
|
|
3734
|
+
* Utilisation legacy (rétrocompatibilité):
|
|
3735
|
+
* <div data-ean="4007123684731"></div> + ViprosSDK.init()
|
|
3736
|
+
*/
|
|
3737
|
+
class ViprosSDK extends EventEmitter {
|
|
3738
|
+
constructor() {
|
|
3739
|
+
super();
|
|
3740
|
+
this.version = '3.0.0-unified';
|
|
3741
|
+
this.isInitialized = false;
|
|
3742
|
+
|
|
3743
|
+
// Configuration par défaut non validée - sera validée au moment de init()
|
|
3744
|
+
this.config = {
|
|
3745
|
+
apiKey: null,
|
|
3746
|
+
debug: false,
|
|
3747
|
+
syncEnabled: true,
|
|
3748
|
+
inlineSyncEnabled: true, // Phase 8: Active la sync inline par défaut (workflow à 1 seul appel)
|
|
3749
|
+
disableBackgroundSync: true, // Phase 8: Désactive les appels POST /sync explicites par défaut
|
|
3750
|
+
containerSelector: '[data-ean]',
|
|
3751
|
+
autoScan: true
|
|
3752
|
+
};
|
|
3753
|
+
|
|
3754
|
+
// Services principaux - Lazy initialization for better performance
|
|
3755
|
+
this._apiClient = null;
|
|
3756
|
+
this._cacheService = null;
|
|
3757
|
+
this._syncService = null; // Service de synchronisation
|
|
3758
|
+
this._servicesInitialized = false;
|
|
3759
|
+
|
|
3760
|
+
// Statistiques globales
|
|
3761
|
+
this.stats = {
|
|
3762
|
+
version: this.version,
|
|
3763
|
+
componentsRegistered: 0,
|
|
3764
|
+
componentsActive: 0,
|
|
3765
|
+
offersLoaded: 0,
|
|
3766
|
+
errors: 0,
|
|
3767
|
+
cacheHits: 0,
|
|
3768
|
+
apiCalls: 0,
|
|
3769
|
+
syncAttempts: 0,
|
|
3770
|
+
syncSuccess: 0
|
|
3771
|
+
};
|
|
3772
|
+
|
|
3773
|
+
// Cache mémoire simple (rétrocompatibilité)
|
|
3774
|
+
this.cache = new Map();
|
|
3775
|
+
|
|
3776
|
+
// Event listeners pour les événements DOM personnalisés
|
|
3777
|
+
this.eventListeners = new Map();
|
|
3778
|
+
|
|
3779
|
+
// Singleton pattern
|
|
3780
|
+
if (ViprosSDK.instance) {
|
|
3781
|
+
return ViprosSDK.instance
|
|
3782
|
+
}
|
|
3783
|
+
ViprosSDK.instance = this;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/**
|
|
3787
|
+
* Méthode statique pour obtenir l'instance singleton
|
|
3788
|
+
*/
|
|
3789
|
+
static getInstance() {
|
|
3790
|
+
if (!ViprosSDK.instance) {
|
|
3791
|
+
ViprosSDK.instance = new ViprosSDK();
|
|
3792
|
+
}
|
|
3793
|
+
return ViprosSDK.instance
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
|
|
3797
|
+
/**
|
|
3798
|
+
* Initialise le SDK avec configuration optionnelle
|
|
3799
|
+
*/
|
|
3800
|
+
init(userConfig = {}) {
|
|
3801
|
+
if (this.isInitialized) {
|
|
3802
|
+
this.debugLog('SDK déjà initialisé');
|
|
3803
|
+
return this
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
// Fusionner avec la nouvelle configuration via ConfigManager
|
|
3807
|
+
this.config = ConfigManager.create({
|
|
3808
|
+
...this.config,
|
|
3809
|
+
...userConfig
|
|
3810
|
+
});
|
|
3811
|
+
|
|
3812
|
+
this.debugLog(`🚀 Initialisation VIPros SDK v${this.version}`);
|
|
3813
|
+
this.debugLog('Configuration:', this.config);
|
|
3814
|
+
|
|
3815
|
+
// Enregistrer le composant Web Components immédiatement
|
|
3816
|
+
this.registerWebComponents();
|
|
3817
|
+
|
|
3818
|
+
// Auto-scan pour rétrocompatibilité si demandé
|
|
3819
|
+
if (this.config.autoScan) {
|
|
3820
|
+
this.scanAndConvert();
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
this.isInitialized = true;
|
|
3824
|
+
this.emitEvent('sdk-ready', { version: this.version, config: this.config });
|
|
3825
|
+
|
|
3826
|
+
this.debugLog('🎯 SDK initialisé avec lazy loading - services créés à la demande');
|
|
3827
|
+
|
|
3828
|
+
return this
|
|
3829
|
+
}
|
|
3830
|
+
|
|
3831
|
+
/**
|
|
3832
|
+
* Lazy getter for ApiClient - created on first access
|
|
3833
|
+
*/
|
|
3834
|
+
get apiClient() {
|
|
3835
|
+
if (!this._apiClient) {
|
|
3836
|
+
this.debugLog('📡 Création lazy d\'ApiClient');
|
|
3837
|
+
this._apiClient = new ApiClient(this.config);
|
|
3838
|
+
}
|
|
3839
|
+
return this._apiClient
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
/**
|
|
3843
|
+
* Lazy getter for CacheService - created on first access
|
|
3844
|
+
*/
|
|
3845
|
+
get cacheService() {
|
|
3846
|
+
if (!this._cacheService) {
|
|
3847
|
+
this.debugLog('💾 Création lazy de CacheService');
|
|
3848
|
+
this._cacheService = new CacheService(this.config.cacheTimeout || 300000);
|
|
3849
|
+
}
|
|
3850
|
+
return this._cacheService
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/**
|
|
3854
|
+
* Lazy getter for SyncService - created on first access
|
|
3855
|
+
*/
|
|
3856
|
+
get syncService() {
|
|
3857
|
+
if (!this._syncService && this.config.syncEnabled) {
|
|
3858
|
+
this.debugLog('🔄 Création lazy d\'EanSyncService');
|
|
3859
|
+
this._syncService = new EanSyncService(this.apiClient, this.cacheService, this.config);
|
|
3860
|
+
|
|
3861
|
+
// Connecter les événements de synchronisation lors de la première création
|
|
3862
|
+
this.setupSyncEvents();
|
|
3863
|
+
}
|
|
3864
|
+
return this._syncService
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
/**
|
|
3868
|
+
* Initialize all services immediately (for cases requiring eager loading)
|
|
3869
|
+
*/
|
|
3870
|
+
initializeServicesEager() {
|
|
3871
|
+
this.debugLog('🚀 Initialisation eager de tous les services...');
|
|
3872
|
+
|
|
3873
|
+
// Access getters to trigger lazy initialization
|
|
3874
|
+
const api = this.apiClient;
|
|
3875
|
+
const cache = this.cacheService;
|
|
3876
|
+
const sync = this.syncService;
|
|
3877
|
+
|
|
3878
|
+
this._servicesInitialized = true;
|
|
3879
|
+
this.debugLog('✅ Tous les services initialisés (eager)');
|
|
3880
|
+
|
|
3881
|
+
return { api, cache, sync }
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
/**
|
|
3885
|
+
* Configure les événements de synchronisation
|
|
3886
|
+
*/
|
|
3887
|
+
setupSyncEvents() {
|
|
3888
|
+
if (!this.syncService) return
|
|
3889
|
+
|
|
3890
|
+
// Rediriger les événements du syncService vers le SDK principal
|
|
3891
|
+
this.syncService.on('syncSuccess', (data) => {
|
|
3892
|
+
this.stats.syncSuccess++;
|
|
3893
|
+
this.emit('syncSuccess', data);
|
|
3894
|
+
});
|
|
3895
|
+
|
|
3896
|
+
this.syncService.on('syncSkipped', (data) => {
|
|
3897
|
+
this.emit('syncSkipped', data);
|
|
3898
|
+
});
|
|
3899
|
+
|
|
3900
|
+
this.syncService.on('syncError', (data) => {
|
|
3901
|
+
this.emit('syncError', data);
|
|
3902
|
+
});
|
|
3903
|
+
|
|
3904
|
+
this.syncService.on('syncRateLimited', (data) => {
|
|
3905
|
+
this.emit('syncRateLimited', data);
|
|
3906
|
+
});
|
|
3907
|
+
|
|
3908
|
+
this.debugLog('Événements de synchronisation connectés');
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
/**
|
|
3912
|
+
* Accès à CacheService pour les Web Components
|
|
3913
|
+
*/
|
|
3914
|
+
getCacheService() {
|
|
3915
|
+
return this.cacheService
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
/**
|
|
3919
|
+
* Déclenche une synchronisation en arrière-plan
|
|
3920
|
+
* Peut être désactivé via config.disableBackgroundSync (Phase 8)
|
|
3921
|
+
*/
|
|
3922
|
+
syncProductInBackground(ean, productUrl) {
|
|
3923
|
+
// Vérifier si la sync en arrière-plan est désactivée (Phase 8)
|
|
3924
|
+
if (this.config.disableBackgroundSync) {
|
|
3925
|
+
this.debugLog('Sync en arrière-plan désactivée (disableBackgroundSync=true)');
|
|
3926
|
+
return
|
|
3927
|
+
}
|
|
3928
|
+
|
|
3929
|
+
if (!this.syncService) {
|
|
3930
|
+
this.debugLog('Sync demandée mais service non disponible');
|
|
3931
|
+
return
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
this.stats.syncAttempts++;
|
|
3935
|
+
this.debugLog(`Déclenchement sync pour EAN ${ean}`);
|
|
3936
|
+
this.syncService.syncProductInBackground(ean, productUrl);
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
/**
|
|
3940
|
+
* Enregistre les Web Components
|
|
3941
|
+
*/
|
|
3942
|
+
registerWebComponents() {
|
|
3943
|
+
if (!customElements.get('vipros-offer-card')) {
|
|
3944
|
+
// Passer le SDK aux composants pour qu'ils puissent accéder à la config et au cache
|
|
3945
|
+
ViprosOfferCard.setSDKInstance(this);
|
|
3946
|
+
customElements.define('vipros-offer-card', ViprosOfferCard);
|
|
3947
|
+
this.stats.componentsRegistered++;
|
|
3948
|
+
this.debugLog('✅ vipros-offer-card registered');
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
/**
|
|
3953
|
+
* Scanne et convertit les anciens containers data-ean (rétrocompatibilité)
|
|
3954
|
+
*/
|
|
3955
|
+
scanAndConvert() {
|
|
3956
|
+
const containers = document.querySelectorAll(this.config.containerSelector);
|
|
3957
|
+
this.debugLog(`🔍 Scan: ${containers.length} containers data-ean trouvés`);
|
|
3958
|
+
|
|
3959
|
+
containers.forEach(container => {
|
|
3960
|
+
const ean = container.getAttribute('data-ean');
|
|
3961
|
+
const price = container.getAttribute('data-price');
|
|
3962
|
+
|
|
3963
|
+
if (ean) {
|
|
3964
|
+
// Créer un Web Component à la place
|
|
3965
|
+
const offerCard = document.createElement('vipros-offer-card');
|
|
3966
|
+
offerCard.setAttribute('ean', ean);
|
|
3967
|
+
if (price) offerCard.setAttribute('price', price);
|
|
3968
|
+
|
|
3969
|
+
// Remplacer le container
|
|
3970
|
+
container.parentNode.replaceChild(offerCard, container);
|
|
3971
|
+
this.debugLog(`🔄 Converti data-ean="${ean}" vers Web Component`);
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
/**
|
|
3977
|
+
* Obtenir l'ApiClient partagé (lazy initialization)
|
|
3978
|
+
* Now uses lazy getter for better performance
|
|
3979
|
+
*/
|
|
3980
|
+
getApiClient() {
|
|
3981
|
+
return this.apiClient // Uses lazy getter
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
/**
|
|
3985
|
+
* Check if services are initialized (performance monitoring)
|
|
3986
|
+
*/
|
|
3987
|
+
getServicesStatus() {
|
|
3988
|
+
return {
|
|
3989
|
+
apiClientInitialized: !!this._apiClient,
|
|
3990
|
+
cacheServiceInitialized: !!this._cacheService,
|
|
3991
|
+
syncServiceInitialized: !!this._syncService,
|
|
3992
|
+
allServicesEager: this._servicesInitialized
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/**
|
|
3997
|
+
* API publique pour récupérer des données produit
|
|
3998
|
+
* Utilise la route optimisée /catalog/items/{ean} via ApiClient
|
|
3999
|
+
*/
|
|
4000
|
+
async getProductData(ean) {
|
|
4001
|
+
const cacheKey = `product_${ean}`;
|
|
4002
|
+
|
|
4003
|
+
// Vérifier le cache mémoire local
|
|
4004
|
+
if (this.cache.has(cacheKey)) {
|
|
4005
|
+
const cached = this.cache.get(cacheKey);
|
|
4006
|
+
if (Date.now() - cached.timestamp < this.config.cacheTimeout) {
|
|
4007
|
+
this.stats.cacheHits++;
|
|
4008
|
+
this.debugLog(`💾 Cache hit for EAN ${ean}`);
|
|
4009
|
+
return cached.data
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
try {
|
|
4014
|
+
this.stats.apiCalls++;
|
|
4015
|
+
|
|
4016
|
+
// Utiliser ApiClient avec la route optimisée /sdk/catalog/items/{ean}
|
|
4017
|
+
// Cela bénéficie du Bloom Filter, rate limiting, et route optimisée
|
|
4018
|
+
const item = await this.apiClient.getCatalogItemByEan(ean);
|
|
4019
|
+
|
|
4020
|
+
// Construire la réponse
|
|
4021
|
+
const data = item ? {
|
|
4022
|
+
code: 200,
|
|
4023
|
+
item: item
|
|
4024
|
+
} : {
|
|
4025
|
+
code: 404,
|
|
4026
|
+
item: null
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
// Mise en cache (seulement si produit trouvé)
|
|
4030
|
+
if (item) {
|
|
4031
|
+
this.cache.set(cacheKey, {
|
|
4032
|
+
data: data,
|
|
4033
|
+
timestamp: Date.now()
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
this.debugLog(`📡 API data loaded for EAN ${ean} (optimized route)`);
|
|
4038
|
+
return data
|
|
4039
|
+
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
this.stats.errors++;
|
|
4042
|
+
this.debugLog(`❌ Erreur API pour EAN ${ean}:`, error);
|
|
4043
|
+
throw error
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
/**
|
|
4048
|
+
* Émet un événement personnalisé
|
|
4049
|
+
*/
|
|
4050
|
+
emitEvent(type, detail = {}) {
|
|
4051
|
+
const eventName = `vipros-${type}`;
|
|
4052
|
+
const event = new CustomEvent(eventName, {
|
|
4053
|
+
detail: detail,
|
|
4054
|
+
bubbles: true,
|
|
4055
|
+
cancelable: true
|
|
4056
|
+
});
|
|
4057
|
+
document.dispatchEvent(event);
|
|
4058
|
+
this.debugLog(`📤 Event: ${eventName}`, detail);
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
/**
|
|
4062
|
+
* Écoute un événement SDK
|
|
4063
|
+
*/
|
|
4064
|
+
on(eventType, callback) {
|
|
4065
|
+
const eventName = `vipros-${eventType}`;
|
|
4066
|
+
document.addEventListener(eventName, callback);
|
|
4067
|
+
|
|
4068
|
+
if (!this.eventListeners.has(eventName)) {
|
|
4069
|
+
this.eventListeners.set(eventName, []);
|
|
4070
|
+
}
|
|
4071
|
+
this.eventListeners.get(eventName).push(callback);
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
/**
|
|
4075
|
+
* Supprime un listener d'événement
|
|
4076
|
+
*/
|
|
4077
|
+
off(eventType, callback) {
|
|
4078
|
+
const eventName = `vipros-${eventType}`;
|
|
4079
|
+
document.removeEventListener(eventName, callback);
|
|
4080
|
+
|
|
4081
|
+
if (this.eventListeners.has(eventName)) {
|
|
4082
|
+
const listeners = this.eventListeners.get(eventName);
|
|
4083
|
+
const index = listeners.indexOf(callback);
|
|
4084
|
+
if (index > -1) {
|
|
4085
|
+
listeners.splice(index, 1);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
/**
|
|
4091
|
+
* Vide le cache
|
|
4092
|
+
*/
|
|
4093
|
+
clearCache() {
|
|
4094
|
+
this.cache.clear();
|
|
4095
|
+
this.debugLog('🗑️ Cache vidé');
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
/**
|
|
4099
|
+
* Force le rechargement de tous les composants
|
|
4100
|
+
*/
|
|
4101
|
+
refreshAll() {
|
|
4102
|
+
const components = document.querySelectorAll('vipros-offer-card');
|
|
4103
|
+
components.forEach(comp => {
|
|
4104
|
+
if (comp.refresh && typeof comp.refresh === 'function') {
|
|
4105
|
+
comp.refresh();
|
|
4106
|
+
}
|
|
4107
|
+
});
|
|
4108
|
+
this.debugLog(`🔄 Refresh de ${components.length} composants`);
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
/**
|
|
4112
|
+
* Récupère les statistiques
|
|
4113
|
+
*/
|
|
4114
|
+
getStats() {
|
|
4115
|
+
this.stats.componentsActive = document.querySelectorAll('vipros-offer-card').length;
|
|
4116
|
+
return { ...this.stats }
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
/**
|
|
4120
|
+
* Log de debug conditionnel
|
|
4121
|
+
*/
|
|
4122
|
+
debugLog(message, data = null) {
|
|
4123
|
+
if (this.config.debug) {
|
|
4124
|
+
if (data) {
|
|
4125
|
+
console.log(`[ViprosSDK] ${message}`, data);
|
|
4126
|
+
} else {
|
|
4127
|
+
console.log(`[ViprosSDK] ${message}`);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
/**
|
|
4133
|
+
* Détruit l'instance SDK (cleanup)
|
|
4134
|
+
* Enhanced to handle lazy-loaded services
|
|
4135
|
+
*/
|
|
4136
|
+
destroy() {
|
|
4137
|
+
// Supprimer tous les event listeners
|
|
4138
|
+
this.eventListeners.forEach((listeners, eventName) => {
|
|
4139
|
+
listeners.forEach(callback => {
|
|
4140
|
+
document.removeEventListener(eventName, callback);
|
|
4141
|
+
});
|
|
4142
|
+
});
|
|
4143
|
+
|
|
4144
|
+
this.eventListeners.clear();
|
|
4145
|
+
this.cache.clear();
|
|
4146
|
+
|
|
4147
|
+
// Cleanup lazy-loaded services
|
|
4148
|
+
if (this._apiClient) {
|
|
4149
|
+
this.debugLog('🧹 Nettoyage ApiClient');
|
|
4150
|
+
this._apiClient = null;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
if (this._cacheService) {
|
|
4154
|
+
this.debugLog('🧹 Nettoyage CacheService');
|
|
4155
|
+
this._cacheService = null;
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
if (this._syncService) {
|
|
4159
|
+
this.debugLog('🧹 Nettoyage SyncService');
|
|
4160
|
+
this._syncService = null;
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
this._servicesInitialized = false;
|
|
4164
|
+
this.isInitialized = false;
|
|
4165
|
+
|
|
4166
|
+
this.debugLog('🔥 SDK détruit avec nettoyage des services lazy');
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
/**
|
|
4171
|
+
* VIPros SDK - Point d'entrée principal
|
|
4172
|
+
*
|
|
4173
|
+
* Version unifiée remplaçant ViprosSDK.js et ViprosWebComponents.js
|
|
4174
|
+
* Compatible avec les deux modes d'utilisation
|
|
4175
|
+
*/
|
|
4176
|
+
|
|
4177
|
+
|
|
4178
|
+
// Create and return an instance directly
|
|
4179
|
+
const ViprosSDKInstance = new ViprosSDK();
|
|
4180
|
+
|
|
4181
|
+
// Pour les builds navigateur : exposer directement l'instance
|
|
4182
|
+
if (typeof window !== 'undefined') {
|
|
4183
|
+
// Assigner l'instance directement à window
|
|
4184
|
+
// Note: Pour les builds minifiés, cette assignation est aussi gérée par le footer du rollup
|
|
4185
|
+
window.ViprosSDK = ViprosSDKInstance;
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
return ViprosSDKInstance;
|
|
4189
|
+
|
|
4190
|
+
})();
|