@xiboplayer/xmr 0.6.10 → 0.6.12

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 CHANGED
@@ -103,9 +103,40 @@ new XmrWrapper(config, player)
103
103
 
104
104
  ## Dependencies
105
105
 
106
- - `@xibosignage/xibo-communication-framework` -- XMR protocol implementation
107
106
  - `@xiboplayer/utils` -- logger
108
107
 
108
+ No external XMR dependencies. The native `XmrClient` (`xmr-client.js`) replaces the
109
+ upstream `@xibosignage/xibo-communication-framework`, providing a complete implementation
110
+ with generic action dispatch and zero third-party dependencies (no luxon, no nanoevents).
111
+
112
+ ### Upstream comparison
113
+
114
+ | Feature | upstream `xibo-communication-framework@0.0.6` | native `XmrClient` |
115
+ |---------|------------------------------------------------|---------------------|
116
+ | `collectNow` | hardcoded | generic dispatch |
117
+ | `screenShot` | hardcoded | generic dispatch |
118
+ | `licenceCheck` | hardcoded | generic dispatch |
119
+ | `criteriaUpdate` | hardcoded | generic dispatch |
120
+ | `commandAction` (showStatusWindow) | hardcoded, splits commandCode | generic dispatch (full message) |
121
+ | `commandAction` (forceUpdateChromeOS) | hardcoded, splits commandCode | generic dispatch (full message) |
122
+ | `commandAction` (currentGeoLocation) | hardcoded, splits commandCode | generic dispatch (full message) |
123
+ | `commandAction` (other) | **missing** -- `console.error('unknown action')` | generic dispatch (full message) |
124
+ | `changeLayout` | **missing** -- `console.error('unknown action')` | generic dispatch |
125
+ | `overlayLayout` | **missing** -- `console.error('unknown action')` | generic dispatch |
126
+ | `revertToSchedule` | **missing** -- `console.error('unknown action')` | generic dispatch |
127
+ | `purgeAll` | **missing** -- `console.error('unknown action')` | generic dispatch |
128
+ | `triggerWebhook` | **missing** -- `console.error('unknown action')` | generic dispatch |
129
+ | `dataUpdate` | **missing** -- `console.error('unknown action')` | generic dispatch |
130
+ | `rekeyAction` | **missing** -- `console.error('unknown action')` | generic dispatch |
131
+ | Any future CMS action | requires library update | works automatically |
132
+ | TTL check | luxon `DateTime.fromISO().plus()` (68KB) | native `Date.parse()` (0KB) |
133
+ | Event emitter | nanoevents (external) | built-in `Map<Set>` (0KB) |
134
+ | Bundle size impact | ~70KB (luxon + nanoevents + framework) | ~2KB |
135
+ | Reconnect interval | 60s `setInterval` | 60s `setInterval` (same) |
136
+ | `isActive()` check | 15min silence threshold | 15min silence threshold (same) |
137
+ | Init handshake | `{type:'init', key, channel}` | `{type:'init', key, channel}` (same) |
138
+ | Heartbeat handling | `"H"` → update lastMessageAt | `"H"` → update lastMessageAt (same) |
139
+
109
140
  ---
110
141
 
111
142
  [xiboplayer.org](https://xiboplayer.org) · **Part of the [XiboPlayer SDK](https://github.com/xibo-players/xiboplayer)**
@@ -119,7 +119,7 @@ xmr.send('rekey');
119
119
 
120
120
  ---
121
121
 
122
- ### criteriaUpdate (New in v0.0.6)
122
+ ### criteriaUpdate (since v0.9.0)
123
123
 
124
124
  Updates display criteria and triggers immediate re-collection.
125
125
 
@@ -142,7 +142,7 @@ await player.collect(); // Gets new criteria from registerDisplay
142
142
 
143
143
  ---
144
144
 
145
- ### currentGeoLocation (New in v0.0.6)
145
+ ### currentGeoLocation (since v0.9.0)
146
146
 
147
147
  Reports current geographic location to CMS.
148
148
 
@@ -285,19 +285,25 @@ To add support for a new XMR command:
285
285
 
286
286
  ## Version History
287
287
 
288
- ### v0.0.6 (Current)
289
- - ✅ Added `criteriaUpdate` command
290
- - ✅ Added `currentGeoLocation` command
288
+ ### v0.10.0 (Current)
289
+ - ✅ Replaced upstream library with native `XmrClient` (generic action dispatch)
290
+ - ✅ All 14 CMS commands supported (upstream only handled 5)
291
+ - ✅ Any future CMS action works automatically (no code changes needed)
292
+ - ✅ Eliminated luxon (68KB) and nanoevents dependencies
293
+ - ✅ 82 tests (26 XmrClient + 56 XmrWrapper)
294
+
295
+ ### v0.9.0
296
+ - ✅ Added `criteriaUpdate` and `currentGeoLocation` commands
291
297
  - ✅ Intentional shutdown flag (no reconnect on stop)
292
- - ✅ Comprehensive test suite (48 tests)
298
+ - ✅ Comprehensive test suite
293
299
 
294
- ### v0.0.5 and earlier
295
- - Basic commands: collectNow, screenShot, changeLayout, licenceCheck, rekey
300
+ ### v0.8.0
301
+ - Initial implementation with full command set
296
302
  - Connection lifecycle management
297
- - Automatic reconnection with exponential backoff
303
+ - Automatic reconnection with 60s health-check interval
298
304
 
299
305
  ## References
300
306
 
301
- - **XMR Library**: @xibosignage/xibo-communication-framework
307
+ - **XMR Client**: Native `XmrClient` (`xmr-client.js`) — full XMR protocol implementation
302
308
  - **Xibo CMS**: https://xibosignage.com
303
309
  - **XMR Protocol**: WebSocket-based push messaging
@@ -32,24 +32,32 @@ npm run test:watch
32
32
 
33
33
  ### Test Coverage
34
34
 
35
- Current coverage: **48 test cases**
36
-
37
- Categories:
38
- - ✅ **Constructor** (2 tests) - Initialization and state
39
- - ✅ **Connection lifecycle** (8 tests) - Start, stop, reconnect
40
- - ✅ **Connection events** (4 tests) - connected, disconnected, error
41
- - ✅ **CMS commands** (14 tests) - All 7 commands + error handling
42
- - ✅ **Reconnection logic** (4 tests) - Exponential backoff, max attempts
43
- - ✅ **stop() method** (4 tests) - Cleanup, error handling
35
+ Current coverage: **82 test cases** (26 XmrClient + 56 XmrWrapper)
36
+
37
+ **XmrClient** (`xmr-client.test.js`):
38
+ - ✅ **start()** (5 tests) - WebSocket open, init handshake, close, error, reconnect
39
+ - ✅ **Message handling** (7 tests) - Heartbeat, JSON dispatch, TTL check, generic dispatch, commandAction, malformed JSON
40
+ - ✅ **isActive()** (3 tests) - Connected + recent, 15min timeout, not connected
41
+ - ✅ **Reconnect interval** (2 tests) - 60s health check, no reconnect after stop
42
+ - ✅ **stop()** (2 tests) - Cleanup, safe when not started
43
+ - ✅ **on()/emit()** (4 tests) - Multiple listeners, unsubscribe, error isolation
44
+ - ✅ **send()** (2 tests) - JSON via WebSocket, throw when disconnected
45
+
46
+ **XmrWrapper** (`xmr-wrapper.test.js`):
47
+ - ✅ **Constructor** (1 test) - Initialization and state
48
+ - ✅ **Connection lifecycle** (7 tests) - Start, stop, reuse, custom channel
49
+ - ✅ **Connection events** (3 tests) - connected, disconnected, error
50
+ - ✅ **CMS commands** (31 tests) - All 13 commands + error handling
51
+ - ✅ **stop() method** (3 tests) - Cleanup, error handling
44
52
  - ✅ **isConnected()** (3 tests) - Connection state queries
45
53
  - ✅ **send()** (4 tests) - Sending messages to CMS
46
54
  - ✅ **Edge cases** (3 tests) - Simultaneous commands, rapid cycles
47
- - ✅ **Memory management** (2 tests) - Timer cleanup, garbage collection
55
+ - ✅ **Memory management** (1 test) - Instance cleanup
48
56
 
49
57
  ### Test Structure
50
58
 
51
59
  Tests use Vitest with:
52
- - **Mocking**: `vi.mock()` for @xibosignage/xibo-communication-framework
60
+ - **Mocking**: `vi.mock()` for `./xmr-client.js` (native XmrClient)
53
61
  - **Fake timers**: `vi.useFakeTimers()` for reconnection testing
54
62
  - **Async handling**: `vi.runAllTimersAsync()` for event handlers
55
63
 
@@ -504,6 +512,6 @@ jobs:
504
512
  ## References
505
513
 
506
514
  - XMR Commands: [XMR_COMMANDS.md](./XMR_COMMANDS.md)
507
- - XMR Library: [@xibosignage/xibo-communication-framework](https://www.npmjs.com/package/@xibosignage/xibo-communication-framework)
515
+ - XMR Client: Native `XmrClient` (`xmr-client.js`) — full XMR protocol implementation
508
516
  - Xibo CMS: https://xibosignage.com
509
517
  - WebSocket Testing: https://github.com/websockets/wscat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/xmr",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "XMR WebSocket client for real-time Xibo CMS commands",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -9,8 +9,7 @@
9
9
  ".": "./src/index.js"
10
10
  },
11
11
  "dependencies": {
12
- "@xibosignage/xibo-communication-framework": "^0.0.6",
13
- "@xiboplayer/utils": "0.6.10"
12
+ "@xiboplayer/utils": "0.6.12"
14
13
  },
15
14
  "devDependencies": {
16
15
  "vitest": "^2.0.0"
package/src/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
1
3
  export const VERSION: string;
2
4
 
3
5
  export class XmrWrapper {
package/src/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
1
3
  // @xiboplayer/xmr - XMR WebSocket client
2
4
  import pkg from '../package.json' with { type: 'json' };
3
5
  export const VERSION = pkg.version;
package/src/test-utils.js CHANGED
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2024-2026 Pau Aliagas <linuxnow@gmail.com>
1
3
  /**
2
4
  * Test Utilities for XMR Package
3
5
  *
@@ -20,13 +22,13 @@ export function createSpy() {
20
22
  }
21
23
 
22
24
  /**
23
- * Mock Xmr class from @xibosignage/xibo-communication-framework
25
+ * Mock XmrClient class (native xmr-client.js)
24
26
  *
25
27
  * Usage:
26
- * const MockXmr = mockXmr();
28
+ * const MockXmrClient = mockXmrClient();
27
29
  * // Use in tests
28
30
  */
29
- export function mockXmr() {
31
+ export function mockXmrClient() {
30
32
  class MockXmr {
31
33
  constructor(channel) {
32
34
  this.channel = channel;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Native XMR (Xibo Message Relay) WebSocket Client
3
+ *
4
+ * Drop-in replacement for @xibosignage/xibo-communication-framework.
5
+ * Uses a generic action dispatcher — emit(message.action, message) — so
6
+ * every CMS action works automatically without a hardcoded if-else chain.
7
+ *
8
+ * API-compatible with the upstream Xmr class:
9
+ * new XmrClient(channel) → .init() → .start(url, key) → .on(event, cb)
10
+ */
11
+
12
+ export class XmrClient {
13
+ /**
14
+ * @param {string} channel - XMR channel identifier (e.g. "player-HWKEY")
15
+ */
16
+ constructor(channel) {
17
+ this.channel = channel;
18
+ this.url = null;
19
+ this.cmsKey = null;
20
+ this.socket = null;
21
+ this.isConnected = false;
22
+ this.isConnectionWanted = false;
23
+ this.lastMessageAt = 0;
24
+ this._interval = null;
25
+ this._listeners = new Map(); // event → Set<callback>
26
+ }
27
+
28
+ /**
29
+ * Register an event listener.
30
+ * @param {string} event
31
+ * @param {Function} callback
32
+ * @returns {Function} Unsubscribe function
33
+ */
34
+ on(event, callback) {
35
+ if (!this._listeners.has(event)) {
36
+ this._listeners.set(event, new Set());
37
+ }
38
+ this._listeners.get(event).add(callback);
39
+ return () => this._listeners.get(event)?.delete(callback);
40
+ }
41
+
42
+ /**
43
+ * Emit an event to all registered listeners.
44
+ * @param {string} event
45
+ * @param {...*} args
46
+ */
47
+ emit(event, ...args) {
48
+ const listeners = this._listeners.get(event);
49
+ if (!listeners) return;
50
+ for (const cb of listeners) {
51
+ try {
52
+ cb(...args);
53
+ } catch (e) {
54
+ console.error(`XmrClient: listener error for '${event}':`, e);
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Initialize the reconnect interval (60s health check).
61
+ * Same cadence as upstream framework.
62
+ */
63
+ async init() {
64
+ if (this._interval) return;
65
+ this._interval = setInterval(() => {
66
+ if (this.isConnectionWanted && !this.isActive()) {
67
+ this.start(this.url || 'DISABLED', this.cmsKey || 'n/a');
68
+ }
69
+ }, 60_000);
70
+ }
71
+
72
+ /**
73
+ * Connect to XMR WebSocket server.
74
+ * @param {string} url - WebSocket URL (ws:// or wss://)
75
+ * @param {string} cmsKey - CMS authentication key
76
+ */
77
+ async start(url, cmsKey) {
78
+ this.url = url;
79
+ this.cmsKey = cmsKey;
80
+ this.isConnectionWanted = true;
81
+
82
+ // Close existing socket if any
83
+ if (this.socket) {
84
+ try { this.socket.close(); } catch (_) { /* ignore */ }
85
+ this.socket = null;
86
+ this.isConnected = false;
87
+ }
88
+
89
+ try {
90
+ this.socket = new WebSocket(url);
91
+ } catch (e) {
92
+ this.emit('error', 'Failed to connect');
93
+ return;
94
+ }
95
+
96
+ this.socket.addEventListener('open', () => {
97
+ this.socket.send(JSON.stringify({
98
+ type: 'init',
99
+ key: this.cmsKey,
100
+ channel: this.channel,
101
+ }));
102
+ this.isConnected = true;
103
+ this.lastMessageAt = Date.now();
104
+ this.emit('connected');
105
+ });
106
+
107
+ this.socket.addEventListener('close', () => {
108
+ this.isConnected = false;
109
+ this.emit('disconnected');
110
+ });
111
+
112
+ this.socket.addEventListener('error', () => {
113
+ this.emit('error', 'error');
114
+ });
115
+
116
+ this.socket.addEventListener('message', (event) => {
117
+ this.lastMessageAt = Date.now();
118
+
119
+ // Heartbeat
120
+ if (event.data === 'H') return;
121
+
122
+ // JSON action message
123
+ try {
124
+ const message = JSON.parse(event.data);
125
+ if (!message.action) return;
126
+
127
+ // TTL check: createdDt (ISO 8601) + ttl seconds > now
128
+ if (message.createdDt && message.ttl) {
129
+ const created = Date.parse(message.createdDt);
130
+ if (!isNaN(created)) {
131
+ const expiresAt = created + parseInt(message.ttl) * 1000;
132
+ if (expiresAt < Date.now()) return; // expired
133
+ }
134
+ }
135
+
136
+ // Generic dispatch — every CMS action works automatically
137
+ this.emit(message.action, message);
138
+ } catch (e) {
139
+ console.error('XmrClient: failed to parse message:', e);
140
+ }
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Stop the connection and clear the reconnect interval.
146
+ */
147
+ async stop() {
148
+ this.isConnectionWanted = false;
149
+ if (this._interval) {
150
+ clearInterval(this._interval);
151
+ this._interval = null;
152
+ }
153
+ if (this.socket) {
154
+ this.socket.close();
155
+ this.socket = null;
156
+ this.isConnected = false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Send a message to the server via WebSocket.
162
+ * @param {string} action - Action name
163
+ * @param {*} data - Data payload
164
+ */
165
+ async send(action, data) {
166
+ if (!this.socket || !this.isConnected) {
167
+ throw new Error('Not connected');
168
+ }
169
+ this.socket.send(JSON.stringify({ action, ...data }));
170
+ }
171
+
172
+ /**
173
+ * Check if the connection is active (connected + message within 15min).
174
+ * @returns {boolean}
175
+ */
176
+ isActive() {
177
+ return this.isConnected && (Date.now() - this.lastMessageAt) < 15 * 60 * 1000;
178
+ }
179
+ }