@xiboplayer/stats 0.7.1 → 0.7.3

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.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Proof of play tracking, stats reporting, and CMS logging",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -10,11 +10,11 @@
10
10
  "./collector": "./src/stats-collector.js"
11
11
  },
12
12
  "dependencies": {
13
- "@xiboplayer/utils": "0.7.1"
13
+ "@xiboplayer/utils": "0.7.3"
14
14
  },
15
15
  "devDependencies": {
16
- "vitest": "^2.0.0",
17
- "fake-indexeddb": "^5.0.2"
16
+ "fake-indexeddb": "^5.0.2",
17
+ "vitest": "^2.1.9"
18
18
  },
19
19
  "keywords": [
20
20
  "xibo",
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
3
+ /**
4
+ * Shared formatting helpers for stats and log XML serialization.
5
+ *
6
+ * @module @xiboplayer/stats/format-helpers
7
+ */
8
+
9
+ /**
10
+ * Format Date object as "YYYY-MM-DD HH:MM:SS"
11
+ * @param {Date|string|number} date
12
+ * @returns {string}
13
+ */
14
+ export function formatDateTime(date) {
15
+ if (!(date instanceof Date)) {
16
+ date = new Date(date);
17
+ }
18
+
19
+ const year = date.getFullYear();
20
+ const month = String(date.getMonth() + 1).padStart(2, '0');
21
+ const day = String(date.getDate()).padStart(2, '0');
22
+ const hours = String(date.getHours()).padStart(2, '0');
23
+ const minutes = String(date.getMinutes()).padStart(2, '0');
24
+ const seconds = String(date.getSeconds()).padStart(2, '0');
25
+
26
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
27
+ }
28
+
29
+ /**
30
+ * Escape XML special characters in a string.
31
+ * Returns non-string values unchanged.
32
+ * @param {string|*} str
33
+ * @returns {string|*}
34
+ */
35
+ export function escapeXml(str) {
36
+ if (typeof str !== 'string') {
37
+ return str;
38
+ }
39
+
40
+ return str
41
+ .replace(/&/g, '&amp;')
42
+ .replace(/</g, '&lt;')
43
+ .replace(/>/g, '&gt;')
44
+ .replace(/"/g, '&quot;')
45
+ .replace(/'/g, '&apos;');
46
+ }
@@ -9,7 +9,8 @@
9
9
  * @module @xiboplayer/stats/logger
10
10
  */
11
11
 
12
- import { createLogger, openIDB } from '@xiboplayer/utils';
12
+ import { createLogger, openIDB, queryByIndex, deleteByIds } from '@xiboplayer/utils';
13
+ import { formatDateTime, escapeXml } from './format-helpers.js';
13
14
 
14
15
  const log = createLogger('@xiboplayer/stats');
15
16
 
@@ -248,45 +249,14 @@ export class LogReporter {
248
249
  return this.log('debug', message, category);
249
250
  }
250
251
 
251
- /** Query records from an IndexedDB index with a cursor, up to a limit. */
252
+ /** @see queryByIndex in @xiboplayer/utils/idb */
252
253
  _queryByIndex(storeName, indexName, keyValue, limit) {
253
- return new Promise((resolve, reject) => {
254
- const tx = this.db.transaction([storeName], 'readonly');
255
- const index = tx.objectStore(storeName).index(indexName);
256
- const request = index.openCursor(keyValue);
257
- const results = [];
258
-
259
- request.onsuccess = (event) => {
260
- const cursor = event.target.result;
261
- if (cursor && results.length < limit) {
262
- results.push(cursor.value);
263
- cursor.continue();
264
- } else {
265
- resolve(results);
266
- }
267
- };
268
- request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
269
- });
254
+ return queryByIndex(this.db, storeName, indexName, keyValue, limit);
270
255
  }
271
256
 
272
- /** Delete records by ID from an IndexedDB object store. */
257
+ /** @see deleteByIds in @xiboplayer/utils/idb */
273
258
  _deleteByIds(storeName, records) {
274
- return new Promise((resolve, reject) => {
275
- const tx = this.db.transaction([storeName], 'readwrite');
276
- const store = tx.objectStore(storeName);
277
- let deleted = 0;
278
-
279
- for (const record of records) {
280
- if (record.id) {
281
- const req = store.delete(record.id);
282
- req.onsuccess = () => { deleted++; };
283
- req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
284
- }
285
- }
286
-
287
- tx.oncomplete = () => resolve(deleted);
288
- tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
289
- });
259
+ return deleteByIds(this.db, storeName, records.map(r => r.id));
290
260
  }
291
261
 
292
262
  /**
@@ -308,25 +278,6 @@ export class LogReporter {
308
278
  return logs;
309
279
  }
310
280
 
311
- /**
312
- * Count unsubmitted logs in the database.
313
- * @returns {Promise<number>}
314
- */
315
- async _countUnsubmitted() {
316
- return new Promise((resolve) => {
317
- try {
318
- const transaction = this.db.transaction([LOGS_STORE], 'readonly');
319
- const store = transaction.objectStore(LOGS_STORE);
320
- const index = store.index('submitted');
321
- const request = index.count(IDBKeyRange.only(0));
322
- request.onsuccess = () => resolve(request.result);
323
- request.onerror = () => resolve(0);
324
- } catch (_) {
325
- resolve(0);
326
- }
327
- });
328
- }
329
-
330
281
  /**
331
282
  * Clear submitted logs from database
332
283
  *
@@ -562,38 +513,3 @@ export function formatFaults(faults) {
562
513
  })));
563
514
  }
564
515
 
565
- /**
566
- * Format Date object as "YYYY-MM-DD HH:MM:SS"
567
- * @private
568
- */
569
- function formatDateTime(date) {
570
- if (!(date instanceof Date)) {
571
- date = new Date(date);
572
- }
573
-
574
- const year = date.getFullYear();
575
- const month = String(date.getMonth() + 1).padStart(2, '0');
576
- const day = String(date.getDate()).padStart(2, '0');
577
- const hours = String(date.getHours()).padStart(2, '0');
578
- const minutes = String(date.getMinutes()).padStart(2, '0');
579
- const seconds = String(date.getSeconds()).padStart(2, '0');
580
-
581
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
582
- }
583
-
584
- /**
585
- * Escape XML special characters
586
- * @private
587
- */
588
- function escapeXml(str) {
589
- if (typeof str !== 'string') {
590
- return str;
591
- }
592
-
593
- return str
594
- .replace(/&/g, '&amp;')
595
- .replace(/</g, '&lt;')
596
- .replace(/>/g, '&gt;')
597
- .replace(/"/g, '&quot;')
598
- .replace(/'/g, '&apos;');
599
- }
@@ -9,7 +9,8 @@
9
9
  * @module @xiboplayer/stats/collector
10
10
  */
11
11
 
12
- import { createLogger, openIDB } from '@xiboplayer/utils';
12
+ import { createLogger, openIDB, queryByIndex, deleteByIds } from '@xiboplayer/utils';
13
+ import { formatDateTime, escapeXml } from './format-helpers.js';
13
14
 
14
15
  const log = createLogger('@xiboplayer/stats');
15
16
 
@@ -309,57 +310,14 @@ export class StatsCollector {
309
310
  }
310
311
  }
311
312
 
312
- /**
313
- * Query records from an IndexedDB index with a cursor, up to a limit.
314
- * @param {string} storeName - Object store name
315
- * @param {string} indexName - Index name
316
- * @param {any} keyValue - Key to query (passed to openCursor)
317
- * @param {number} limit - Maximum records to return
318
- * @returns {Promise<Array>}
319
- */
313
+ /** @see queryByIndex in @xiboplayer/utils/idb */
320
314
  _queryByIndex(storeName, indexName, keyValue, limit) {
321
- return new Promise((resolve, reject) => {
322
- const tx = this.db.transaction([storeName], 'readonly');
323
- const index = tx.objectStore(storeName).index(indexName);
324
- const request = index.openCursor(keyValue);
325
- const results = [];
326
-
327
- request.onsuccess = (event) => {
328
- const cursor = event.target.result;
329
- if (cursor && results.length < limit) {
330
- results.push(cursor.value);
331
- cursor.continue();
332
- } else {
333
- resolve(results);
334
- }
335
- };
336
- request.onerror = () => reject(new Error(`Index query failed: ${request.error}`));
337
- });
315
+ return queryByIndex(this.db, storeName, indexName, keyValue, limit);
338
316
  }
339
317
 
340
- /**
341
- * Delete records by ID from an IndexedDB object store.
342
- * @param {string} storeName - Object store name
343
- * @param {Array} records - Records with .id property
344
- * @returns {Promise<number>} Number of deleted records
345
- */
318
+ /** @see deleteByIds in @xiboplayer/utils/idb */
346
319
  _deleteByIds(storeName, records) {
347
- return new Promise((resolve, reject) => {
348
- const tx = this.db.transaction([storeName], 'readwrite');
349
- const store = tx.objectStore(storeName);
350
- let deleted = 0;
351
-
352
- for (const record of records) {
353
- if (record.id) {
354
- const req = store.delete(record.id);
355
- req.onsuccess = () => { deleted++; };
356
- req.onerror = () => { log.error(`Failed to delete ${record.id}:`, req.error); };
357
- }
358
- }
359
-
360
- tx.oncomplete = () => resolve(deleted);
361
- tx.onerror = () => reject(new Error(`Delete failed: ${tx.error}`));
362
- });
320
+ return deleteByIds(this.db, storeName, records.map(r => r.id));
363
321
  }
364
322
 
365
323
  /**
@@ -704,38 +662,3 @@ export function formatStats(stats) {
704
662
  return `<stats>\n${statElements.join('\n')}\n</stats>`;
705
663
  }
706
664
 
707
- /**
708
- * Format Date object as "YYYY-MM-DD HH:MM:SS"
709
- * @private
710
- */
711
- function formatDateTime(date) {
712
- if (!(date instanceof Date)) {
713
- date = new Date(date);
714
- }
715
-
716
- const year = date.getFullYear();
717
- const month = String(date.getMonth() + 1).padStart(2, '0');
718
- const day = String(date.getDate()).padStart(2, '0');
719
- const hours = String(date.getHours()).padStart(2, '0');
720
- const minutes = String(date.getMinutes()).padStart(2, '0');
721
- const seconds = String(date.getSeconds()).padStart(2, '0');
722
-
723
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
724
- }
725
-
726
- /**
727
- * Escape XML special characters
728
- * @private
729
- */
730
- function escapeXml(str) {
731
- if (typeof str !== 'string') {
732
- return str;
733
- }
734
-
735
- return str
736
- .replace(/&/g, '&amp;')
737
- .replace(/</g, '&lt;')
738
- .replace(/>/g, '&gt;')
739
- .replace(/"/g, '&quot;')
740
- .replace(/'/g, '&apos;');
741
- }