@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/stats",
3
- "version": "0.6.1",
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.1"
13
+ "@xiboplayer/utils": "0.6.2"
14
14
  },
15
15
  "devDependencies": {
16
16
  "vitest": "^2.0.0",
@@ -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
- return new Promise((resolve, reject) => {
289
- const transaction = this.db.transaction([LOGS_STORE], 'readonly');
290
- const store = transaction.objectStore(LOGS_STORE);
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
- log.warn('Logs database not initialized');
346
- return;
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
  /**
@@ -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
- return new Promise((resolve, reject) => {
351
- const transaction = this.db.transaction([STATS_STORE], 'readonly');
352
- const store = transaction.objectStore(STATS_STORE);
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
- log.warn('Stats database not initialized');
389
- return;
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
  /**