@xiboplayer/stats 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/log-reporter.js +47 -63
- package/src/stats-collector.js +59 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/stats",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Proof of play tracking, stats reporting, and CMS logging",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./collector": "./src/stats-collector.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@xiboplayer/utils": "0.6.
|
|
13
|
+
"@xiboplayer/utils": "0.6.2"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"vitest": "^2.0.0",
|
package/src/log-reporter.js
CHANGED
|
@@ -271,6 +271,47 @@ export class LogReporter {
|
|
|
271
271
|
return this.log('debug', message, category);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/** Query records from an IndexedDB index with a cursor, up to a limit. */
|
|
275
|
+
_queryByIndex(storeName, indexName, keyValue, limit) {
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const tx = this.db.transaction([storeName], 'readonly');
|
|
278
|
+
const index = tx.objectStore(storeName).index(indexName);
|
|
279
|
+
const request = index.openCursor(keyValue);
|
|
280
|
+
const results = [];
|
|
281
|
+
|
|
282
|
+
request.onsuccess = (event) => {
|
|
283
|
+
const cursor = event.target.result;
|
|
284
|
+
if (cursor && results.length < limit) {
|
|
285
|
+
results.push(cursor.value);
|
|
286
|
+
cursor.continue();
|
|
287
|
+
} else {
|
|
288
|
+
resolve(results);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Delete records by ID from an IndexedDB object store. */
|
|
296
|
+
_deleteByIds(storeName, records) {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
const tx = this.db.transaction([storeName], 'readwrite');
|
|
299
|
+
const store = tx.objectStore(storeName);
|
|
300
|
+
let deleted = 0;
|
|
301
|
+
|
|
302
|
+
for (const record of records) {
|
|
303
|
+
if (record.id) {
|
|
304
|
+
const req = store.delete(record.id);
|
|
305
|
+
req.onsuccess = () => { deleted++; };
|
|
306
|
+
req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
tx.oncomplete = () => resolve(deleted);
|
|
311
|
+
tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
274
315
|
/**
|
|
275
316
|
* Get logs ready for submission to CMS
|
|
276
317
|
*
|
|
@@ -285,32 +326,9 @@ export class LogReporter {
|
|
|
285
326
|
return [];
|
|
286
327
|
}
|
|
287
328
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const index = store.index('submitted');
|
|
292
|
-
|
|
293
|
-
// Query for unsubmitted logs (0 = false)
|
|
294
|
-
const request = index.openCursor(IDBKeyRange.only(0));
|
|
295
|
-
const logs = [];
|
|
296
|
-
|
|
297
|
-
request.onsuccess = (event) => {
|
|
298
|
-
const cursor = event.target.result;
|
|
299
|
-
|
|
300
|
-
if (cursor && logs.length < limit) {
|
|
301
|
-
logs.push(cursor.value);
|
|
302
|
-
cursor.continue();
|
|
303
|
-
} else {
|
|
304
|
-
log.debug(`Retrieved ${logs.length} unsubmitted logs (limit: ${limit})`);
|
|
305
|
-
resolve(logs);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
request.onerror = () => {
|
|
310
|
-
log.error('Failed to retrieve logs:', request.error);
|
|
311
|
-
reject(new Error(`Failed to retrieve logs: ${request.error}`));
|
|
312
|
-
};
|
|
313
|
-
});
|
|
329
|
+
const logs = await this._queryByIndex(LOGS_STORE, 'submitted', IDBKeyRange.only(0), limit);
|
|
330
|
+
log.debug(`Retrieved ${logs.length} unsubmitted logs (limit: ${limit})`);
|
|
331
|
+
return logs;
|
|
314
332
|
}
|
|
315
333
|
|
|
316
334
|
/**
|
|
@@ -341,43 +359,9 @@ export class LogReporter {
|
|
|
341
359
|
* @returns {Promise<void>}
|
|
342
360
|
*/
|
|
343
361
|
async clearSubmittedLogs(logs) {
|
|
344
|
-
if (!this.db)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (!logs || logs.length === 0) {
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return new Promise((resolve, reject) => {
|
|
354
|
-
const transaction = this.db.transaction([LOGS_STORE], 'readwrite');
|
|
355
|
-
const store = transaction.objectStore(LOGS_STORE);
|
|
356
|
-
|
|
357
|
-
let deletedCount = 0;
|
|
358
|
-
|
|
359
|
-
logs.forEach((logEntry) => {
|
|
360
|
-
if (logEntry.id) {
|
|
361
|
-
const request = store.delete(logEntry.id);
|
|
362
|
-
request.onsuccess = () => {
|
|
363
|
-
deletedCount++;
|
|
364
|
-
};
|
|
365
|
-
request.onerror = () => {
|
|
366
|
-
log.error(`Failed to delete log ${logEntry.id}:`, request.error);
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
transaction.oncomplete = () => {
|
|
372
|
-
log.debug(`Deleted ${deletedCount} submitted logs`);
|
|
373
|
-
resolve();
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
transaction.onerror = () => {
|
|
377
|
-
log.error('Failed to delete submitted logs:', transaction.error);
|
|
378
|
-
reject(new Error(`Failed to delete logs: ${transaction.error}`));
|
|
379
|
-
};
|
|
380
|
-
});
|
|
362
|
+
if (!this.db || !logs?.length) return;
|
|
363
|
+
const deleted = await this._deleteByIds(LOGS_STORE, logs);
|
|
364
|
+
log.debug(`Deleted ${deleted} submitted logs`);
|
|
381
365
|
}
|
|
382
366
|
|
|
383
367
|
/**
|
package/src/stats-collector.js
CHANGED
|
@@ -332,6 +332,59 @@ export class StatsCollector {
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Query records from an IndexedDB index with a cursor, up to a limit.
|
|
337
|
+
* @param {string} storeName - Object store name
|
|
338
|
+
* @param {string} indexName - Index name
|
|
339
|
+
* @param {any} keyValue - Key to query (passed to openCursor)
|
|
340
|
+
* @param {number} limit - Maximum records to return
|
|
341
|
+
* @returns {Promise<Array>}
|
|
342
|
+
*/
|
|
343
|
+
_queryByIndex(storeName, indexName, keyValue, limit) {
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
const tx = this.db.transaction([storeName], 'readonly');
|
|
346
|
+
const index = tx.objectStore(storeName).index(indexName);
|
|
347
|
+
const request = index.openCursor(keyValue);
|
|
348
|
+
const results = [];
|
|
349
|
+
|
|
350
|
+
request.onsuccess = (event) => {
|
|
351
|
+
const cursor = event.target.result;
|
|
352
|
+
if (cursor && results.length < limit) {
|
|
353
|
+
results.push(cursor.value);
|
|
354
|
+
cursor.continue();
|
|
355
|
+
} else {
|
|
356
|
+
resolve(results);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Delete records by ID from an IndexedDB object store.
|
|
365
|
+
* @param {string} storeName - Object store name
|
|
366
|
+
* @param {Array} records - Records with .id property
|
|
367
|
+
* @returns {Promise<number>} Number of deleted records
|
|
368
|
+
*/
|
|
369
|
+
_deleteByIds(storeName, records) {
|
|
370
|
+
return new Promise((resolve, reject) => {
|
|
371
|
+
const tx = this.db.transaction([storeName], 'readwrite');
|
|
372
|
+
const store = tx.objectStore(storeName);
|
|
373
|
+
let deleted = 0;
|
|
374
|
+
|
|
375
|
+
for (const record of records) {
|
|
376
|
+
if (record.id) {
|
|
377
|
+
const req = store.delete(record.id);
|
|
378
|
+
req.onsuccess = () => { deleted++; };
|
|
379
|
+
req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
tx.oncomplete = () => resolve(deleted);
|
|
384
|
+
tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
335
388
|
/**
|
|
336
389
|
* Get stats ready for submission to CMS
|
|
337
390
|
*
|
|
@@ -347,32 +400,9 @@ export class StatsCollector {
|
|
|
347
400
|
return [];
|
|
348
401
|
}
|
|
349
402
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const index = store.index('submitted');
|
|
354
|
-
|
|
355
|
-
// Query for unsubmitted stats (0 = false)
|
|
356
|
-
const request = index.openCursor(IDBKeyRange.only(0));
|
|
357
|
-
const stats = [];
|
|
358
|
-
|
|
359
|
-
request.onsuccess = (event) => {
|
|
360
|
-
const cursor = event.target.result;
|
|
361
|
-
|
|
362
|
-
if (cursor && stats.length < limit) {
|
|
363
|
-
stats.push(cursor.value);
|
|
364
|
-
cursor.continue();
|
|
365
|
-
} else {
|
|
366
|
-
log.debug(`Retrieved ${stats.length} unsubmitted stats`);
|
|
367
|
-
resolve(stats);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
request.onerror = () => {
|
|
372
|
-
log.error('Failed to retrieve stats:', request.error);
|
|
373
|
-
reject(new Error(`Failed to retrieve stats: ${request.error}`));
|
|
374
|
-
};
|
|
375
|
-
});
|
|
403
|
+
const stats = await this._queryByIndex(STATS_STORE, 'submitted', IDBKeyRange.only(0), limit);
|
|
404
|
+
log.debug(`Retrieved ${stats.length} unsubmitted stats`);
|
|
405
|
+
return stats;
|
|
376
406
|
}
|
|
377
407
|
|
|
378
408
|
/**
|
|
@@ -384,43 +414,9 @@ export class StatsCollector {
|
|
|
384
414
|
* @returns {Promise<void>}
|
|
385
415
|
*/
|
|
386
416
|
async clearSubmittedStats(stats) {
|
|
387
|
-
if (!this.db)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (!stats || stats.length === 0) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return new Promise((resolve, reject) => {
|
|
397
|
-
const transaction = this.db.transaction([STATS_STORE], 'readwrite');
|
|
398
|
-
const store = transaction.objectStore(STATS_STORE);
|
|
399
|
-
|
|
400
|
-
let deletedCount = 0;
|
|
401
|
-
|
|
402
|
-
stats.forEach((stat) => {
|
|
403
|
-
if (stat.id) {
|
|
404
|
-
const request = store.delete(stat.id);
|
|
405
|
-
request.onsuccess = () => {
|
|
406
|
-
deletedCount++;
|
|
407
|
-
};
|
|
408
|
-
request.onerror = () => {
|
|
409
|
-
log.error(`Failed to delete stat ${stat.id}:`, request.error);
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
transaction.oncomplete = () => {
|
|
415
|
-
log.debug(`Deleted ${deletedCount} submitted stats`);
|
|
416
|
-
resolve();
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
transaction.onerror = () => {
|
|
420
|
-
log.error('Failed to delete submitted stats:', transaction.error);
|
|
421
|
-
reject(new Error(`Failed to delete stats: ${transaction.error}`));
|
|
422
|
-
};
|
|
423
|
-
});
|
|
417
|
+
if (!this.db || !stats?.length) return;
|
|
418
|
+
const deleted = await this._deleteByIds(STATS_STORE, stats);
|
|
419
|
+
log.debug(`Deleted ${deleted} submitted stats`);
|
|
424
420
|
}
|
|
425
421
|
|
|
426
422
|
/**
|