@xiboplayer/xmds 0.6.3 → 0.6.5
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 +177 -23
- package/package.json +2 -2
- package/src/cms-client.js +89 -0
- package/src/cms-client.test.js +51 -0
- package/src/index.js +1 -0
- package/src/protocol-detector.js +18 -8
- package/src/protocol-detector.test.js +10 -0
- package/src/rest-client.js +1 -1
- package/src/xmds.rest.integration.test.js +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,42 @@
|
|
|
1
1
|
# @xiboplayer/xmds
|
|
2
2
|
|
|
3
|
-
**XMDS
|
|
3
|
+
**XMDS/REST dual-transport CMS client for Xibo digital signage -- auto-detects REST or SOAP based on CMS capabilities.**
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Unified abstraction over Xibo's two communication protocols:
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
9
|
+
- **REST API v2** -- JSON-based, JWT auth, ETag caching, ~30% smaller payloads
|
|
10
|
+
- **XMDS SOAP** (v3-v7) -- XML-based, universal compatibility with all Xibo CMS versions
|
|
11
11
|
|
|
12
|
-
Both
|
|
12
|
+
Both expose an identical public API. At startup, `ProtocolDetector` probes the CMS to select the optimal transport -- fallback to SOAP if REST is unavailable.
|
|
13
|
+
|
|
14
|
+
### Capabilities
|
|
15
|
+
|
|
16
|
+
- **Dual-transport abstraction** -- RestClient and XmdsClient implement the same interface
|
|
17
|
+
- **Auto-detection** -- quick health probe (3s timeout) to select REST or SOAP
|
|
18
|
+
- **HTTP caching** -- REST client uses ETags to avoid redundant GETs
|
|
19
|
+
- **Retry & backoff** -- exponential backoff with jitter (default: 2 retries, 2s base delay)
|
|
20
|
+
- **JWT auth** -- REST client auto-refreshes tokens 60s before expiry
|
|
21
|
+
- **CRC checksums** -- `checkRf` and `checkSchedule` allow skipping unchanged data
|
|
22
|
+
- **Delegated reporting** -- stats/logs can be submitted on behalf of other displays (follower -> lead delegation)
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Player Core Transport Selection CMS
|
|
28
|
+
----------- -------------------- -----
|
|
29
|
+
|
|
30
|
+
registerDisplay() Is REST available?
|
|
31
|
+
requiredFiles() Same API (GET /api/v2/player/health)
|
|
32
|
+
schedule() Yes --> RestClient (JWT, ETag) --> /api/v2/player/*
|
|
33
|
+
getResource() No --> XmdsClient (SOAP XML) --> /xmds.php
|
|
34
|
+
notify()
|
|
35
|
+
submitStats()
|
|
36
|
+
submitLog()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both clients share the same return types. The scheduler, renderer, and sync modules consume only the CmsClient interface -- they're transport-agnostic.
|
|
13
40
|
|
|
14
41
|
## Installation
|
|
15
42
|
|
|
@@ -19,37 +46,164 @@ npm install @xiboplayer/xmds
|
|
|
19
46
|
|
|
20
47
|
## Usage
|
|
21
48
|
|
|
49
|
+
### Auto-detect and instantiate
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
import { ProtocolDetector, RestClient, XmdsClient } from '@xiboplayer/xmds';
|
|
53
|
+
|
|
54
|
+
const detector = new ProtocolDetector(cmsUrl, RestClient, XmdsClient);
|
|
55
|
+
const { client, protocol } = await detector.detect({
|
|
56
|
+
cmsUrl: 'https://cms.example.com',
|
|
57
|
+
cmsKey: 'your-server-key',
|
|
58
|
+
hardwareKey: 'display-123',
|
|
59
|
+
displayName: 'Main Screen',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(`Using ${protocol} transport`);
|
|
63
|
+
|
|
64
|
+
const display = await client.registerDisplay();
|
|
65
|
+
const files = await client.requiredFiles();
|
|
66
|
+
const schedule = await client.schedule();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Force a specific transport
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const { client } = await detector.detect(config, 'xmds'); // Force SOAP
|
|
73
|
+
const { client } = await detector.detect(config, 'rest'); // Force REST
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Direct RestClient usage
|
|
77
|
+
|
|
22
78
|
```javascript
|
|
23
79
|
import { RestClient } from '@xiboplayer/xmds';
|
|
24
80
|
|
|
25
81
|
const client = new RestClient({
|
|
26
|
-
cmsUrl: 'https://
|
|
27
|
-
|
|
28
|
-
hardwareKey: 'display-
|
|
82
|
+
cmsUrl: 'https://cms.example.com',
|
|
83
|
+
cmsKey: 'server-key',
|
|
84
|
+
hardwareKey: 'display-key',
|
|
85
|
+
displayName: 'Display 1',
|
|
29
86
|
});
|
|
30
87
|
|
|
31
|
-
const
|
|
32
|
-
const files = await client.requiredFiles();
|
|
33
|
-
|
|
88
|
+
const { code, message, settings, syncConfig } = await client.registerDisplay();
|
|
89
|
+
const { files, purge } = await client.requiredFiles();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Direct XmdsClient usage
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
import { XmdsClient } from '@xiboplayer/xmds';
|
|
96
|
+
|
|
97
|
+
const client = new XmdsClient({
|
|
98
|
+
cmsUrl: 'https://cms.example.com',
|
|
99
|
+
cmsKey: 'server-key',
|
|
100
|
+
hardwareKey: 'display-key',
|
|
101
|
+
displayName: 'Display 1',
|
|
102
|
+
xmrChannel: 'ch-123',
|
|
103
|
+
xmrPubKey: '-----BEGIN PUBLIC KEY-----\n...',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const display = await client.registerDisplay();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Parse schedule without network call
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { parseScheduleResponse } from '@xiboplayer/xmds';
|
|
113
|
+
|
|
114
|
+
const parsed = parseScheduleResponse(scheduleXml);
|
|
115
|
+
// { default, layouts, campaigns, overlays, actions, commands, dataConnectors }
|
|
34
116
|
```
|
|
35
117
|
|
|
36
118
|
## Methods
|
|
37
119
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
|
41
|
-
|
|
42
|
-
| `
|
|
43
|
-
| `
|
|
44
|
-
| `
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
120
|
+
All methods available on both `RestClient` and `XmdsClient` with identical signatures:
|
|
121
|
+
|
|
122
|
+
| Method | Parameters | Returns | Description |
|
|
123
|
+
|--------|------------|---------|-------------|
|
|
124
|
+
| `registerDisplay()` | -- | `RegisterDisplayResult` | Authenticate, get settings, tags, commands, sync config |
|
|
125
|
+
| `requiredFiles()` | -- | `RequiredFilesResult` | Get media, layouts, widgets, and files to purge |
|
|
126
|
+
| `schedule()` | -- | `ScheduleObject` | Get complete schedule |
|
|
127
|
+
| `getResource(layoutId, regionId, mediaId)` | number x 3 | `string` | Get rendered widget HTML |
|
|
128
|
+
| `notifyStatus(status)` | Object | JSON/XML | Report display status |
|
|
129
|
+
| `mediaInventory(inventoryXml)` | string/Array | JSON/XML | Report cached media |
|
|
130
|
+
| `submitStats(statsXml, hardwareKeyOverride?)` | string, string? | boolean | Submit proof-of-play (optional override for delegated reporting) |
|
|
131
|
+
| `submitLog(logXml, hardwareKeyOverride?)` | string, string? | boolean | Submit logs (optional override for delegated reporting) |
|
|
132
|
+
| `submitScreenShot(base64Image)` | string | boolean | Upload screenshot |
|
|
133
|
+
| `reportFaults(faultJson)` | string/Object | boolean | Report hardware/software faults |
|
|
134
|
+
| `blackList(mediaId, type, reason)` | string, string, string | boolean | Blacklist broken media |
|
|
135
|
+
| `getWeather()` | -- | JSON/XML | Get weather data for schedule criteria |
|
|
136
|
+
|
|
137
|
+
### Key Response Types
|
|
138
|
+
|
|
139
|
+
**RegisterDisplayResult:**
|
|
140
|
+
```typescript
|
|
141
|
+
{
|
|
142
|
+
code: 'READY' | 'WRONG_SCHEDULE_KEY' | 'DISPLAY_NOT_LICENSED' | ...,
|
|
143
|
+
message: string,
|
|
144
|
+
settings: { [key: string]: any },
|
|
145
|
+
tags: string[],
|
|
146
|
+
commands: Array<{ commandCode, commandString }>,
|
|
147
|
+
checkRf: string, // CRC32 of RequiredFiles (skip if unchanged)
|
|
148
|
+
checkSchedule: string, // CRC32 of Schedule (skip if unchanged)
|
|
149
|
+
syncConfig: { // Multi-display sync (null if not enabled)
|
|
150
|
+
syncGroup: string,
|
|
151
|
+
syncPublisherPort: number,
|
|
152
|
+
syncSwitchDelay: number,
|
|
153
|
+
isLead: boolean
|
|
154
|
+
} | null
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Transport Comparison
|
|
159
|
+
|
|
160
|
+
| Aspect | REST (v2) | XMDS (SOAP) |
|
|
161
|
+
|--------|-----------|-------------|
|
|
162
|
+
| **Protocol** | JSON over HTTP | XML-RPC over HTTP |
|
|
163
|
+
| **Auth** | JWT (Bearer token) | Per-request params (serverKey) |
|
|
164
|
+
| **Payload size** | ~30% smaller | Baseline |
|
|
165
|
+
| **Caching** | ETags + response cache | No caching |
|
|
166
|
+
| **Availability** | Custom CMS images (Xibo 3.0+) | All Xibo versions (v3-v7+) |
|
|
167
|
+
| **Fallback** | SOAP (automatic) | None |
|
|
168
|
+
|
|
169
|
+
## Error Handling
|
|
170
|
+
|
|
171
|
+
**Retry:** Both clients use `fetchWithRetry()` with exponential backoff (2 retries, 2s base delay).
|
|
172
|
+
|
|
173
|
+
**Token expiry (REST):** 401 response triggers automatic re-authentication and request retry.
|
|
174
|
+
|
|
175
|
+
**SOAP faults:** XmdsClient parses `<soap:Fault>` and throws with fault message.
|
|
176
|
+
|
|
177
|
+
**Custom retry strategy:**
|
|
178
|
+
```javascript
|
|
179
|
+
const client = new RestClient({
|
|
180
|
+
...config,
|
|
181
|
+
retryOptions: { maxRetries: 5, baseDelayMs: 5000 }
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Constructor Options
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
{
|
|
189
|
+
cmsUrl: string, // Base URL of Xibo CMS
|
|
190
|
+
cmsKey: string, // Server authentication key
|
|
191
|
+
hardwareKey: string, // Unique display identifier
|
|
192
|
+
displayName?: string, // Human-readable display name
|
|
193
|
+
clientVersion?: string, // Default: '0.1.0'
|
|
194
|
+
clientType?: string, // Default: 'linux'
|
|
195
|
+
xmrChannel?: string, // XMR channel ID
|
|
196
|
+
xmrPubKey?: string, // XMR public key (PEM)
|
|
197
|
+
retryOptions?: {
|
|
198
|
+
maxRetries?: number, // Default: 2
|
|
199
|
+
baseDelayMs?: number // Default: 2000
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
49
203
|
|
|
50
204
|
## Dependencies
|
|
51
205
|
|
|
52
|
-
- `@xiboplayer/utils`
|
|
206
|
+
- `@xiboplayer/utils` -- logger, fetchWithRetry
|
|
53
207
|
|
|
54
208
|
---
|
|
55
209
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/xmds",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "XMDS SOAP client for Xibo CMS communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"./schedule-parser": "./src/schedule-parser.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@xiboplayer/utils": "0.6.
|
|
15
|
+
"@xiboplayer/utils": "0.6.5"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CmsClient interface definition — the unified contract for CMS communication.
|
|
3
|
+
*
|
|
4
|
+
* Both RestClient and XmdsClient implement this interface identically.
|
|
5
|
+
* PlayerCore calls these methods without knowing which transport is underneath.
|
|
6
|
+
* ProtocolDetector selects the implementation at startup.
|
|
7
|
+
*
|
|
8
|
+
* This file provides:
|
|
9
|
+
* 1. JSDoc type definitions for the interface
|
|
10
|
+
* 2. CMS_CLIENT_METHODS — canonical list of required methods
|
|
11
|
+
* 3. assertCmsClient() — runtime conformance check
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { ProtocolDetector, RestClient, XmdsClient } from '@xiboplayer/xmds';
|
|
15
|
+
*
|
|
16
|
+
* const detector = new ProtocolDetector(cmsUrl, RestClient, XmdsClient);
|
|
17
|
+
* const { client } = await detector.detect(config);
|
|
18
|
+
* // client implements CmsClient — PlayerCore doesn't care which one
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} RegisterDisplayResult
|
|
23
|
+
* @property {string} code - 'READY' or error code
|
|
24
|
+
* @property {string} message - Human-readable status message
|
|
25
|
+
* @property {Object|null} settings - Display settings (null if not READY)
|
|
26
|
+
* @property {string[]} tags - Display tags
|
|
27
|
+
* @property {Array<{commandCode: string, commandString: string}>} commands - Scheduled commands
|
|
28
|
+
* @property {Object} displayAttrs - Server-provided attributes (date, timezone, status)
|
|
29
|
+
* @property {string} checkRf - CRC checksum for RequiredFiles (skip if unchanged)
|
|
30
|
+
* @property {string} checkSchedule - CRC checksum for Schedule (skip if unchanged)
|
|
31
|
+
* @property {Object|null} syncConfig - Multi-display sync configuration
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} RequiredFilesResult
|
|
36
|
+
* @property {Array<{type: string, id: string, size: number, md5: string, download: string, path: string, saveAs: string|null}>} files
|
|
37
|
+
* @property {Array<{id: string, storedAs: string}>} purge - Files to delete
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} CmsClient
|
|
42
|
+
* @property {() => Promise<RegisterDisplayResult>} registerDisplay
|
|
43
|
+
* @property {() => Promise<RequiredFilesResult>} requiredFiles
|
|
44
|
+
* @property {() => Promise<Object>} schedule
|
|
45
|
+
* @property {(layoutId: string, regionId: string, mediaId: string) => Promise<string>} getResource
|
|
46
|
+
* @property {(status: Object) => Promise<any>} notifyStatus
|
|
47
|
+
* @property {(inventoryXml: string|Array) => Promise<any>} mediaInventory
|
|
48
|
+
* @property {(mediaId: string, type: string, reason: string) => Promise<boolean>} blackList
|
|
49
|
+
* @property {(logXml: string|Array, hardwareKeyOverride?: string) => Promise<boolean>} submitLog
|
|
50
|
+
* @property {(base64Image: string) => Promise<boolean>} submitScreenShot
|
|
51
|
+
* @property {(statsXml: string|Array, hardwareKeyOverride?: string) => Promise<boolean>} submitStats
|
|
52
|
+
* @property {(faultJson: string|Object) => Promise<boolean>} reportFaults
|
|
53
|
+
* @property {() => Promise<Object>} getWeather
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Canonical list of methods that every CmsClient implementation must provide.
|
|
58
|
+
* Used for runtime conformance checks and test assertions.
|
|
59
|
+
*/
|
|
60
|
+
export const CMS_CLIENT_METHODS = [
|
|
61
|
+
'registerDisplay',
|
|
62
|
+
'requiredFiles',
|
|
63
|
+
'schedule',
|
|
64
|
+
'getResource',
|
|
65
|
+
'notifyStatus',
|
|
66
|
+
'mediaInventory',
|
|
67
|
+
'blackList',
|
|
68
|
+
'submitLog',
|
|
69
|
+
'submitScreenShot',
|
|
70
|
+
'submitStats',
|
|
71
|
+
'reportFaults',
|
|
72
|
+
'getWeather',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verify that an object implements the CmsClient interface.
|
|
77
|
+
* Throws if any required method is missing or not a function.
|
|
78
|
+
*
|
|
79
|
+
* @param {any} client - Object to check
|
|
80
|
+
* @param {string} [label] - Label for error messages (e.g. 'RestClient')
|
|
81
|
+
* @throws {Error} If the client doesn't conform
|
|
82
|
+
*/
|
|
83
|
+
export function assertCmsClient(client, label = 'client') {
|
|
84
|
+
for (const method of CMS_CLIENT_METHODS) {
|
|
85
|
+
if (typeof client[method] !== 'function') {
|
|
86
|
+
throw new Error(`${label} missing CmsClient method: ${method}()`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { CMS_CLIENT_METHODS, assertCmsClient } from './cms-client.js';
|
|
4
|
+
import { RestClient } from './rest-client.js';
|
|
5
|
+
import { XmdsClient } from './xmds-client.js';
|
|
6
|
+
|
|
7
|
+
const mockConfig = {
|
|
8
|
+
cmsUrl: 'https://example.com',
|
|
9
|
+
cmsKey: 'test',
|
|
10
|
+
hardwareKey: 'hw-001',
|
|
11
|
+
displayName: 'Test Display',
|
|
12
|
+
xmrChannel: '',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('CmsClient interface conformance', () => {
|
|
16
|
+
const restClient = new RestClient(mockConfig);
|
|
17
|
+
const xmdsClient = new XmdsClient(mockConfig);
|
|
18
|
+
|
|
19
|
+
it('CMS_CLIENT_METHODS lists 12 methods', () => {
|
|
20
|
+
expect(CMS_CLIENT_METHODS).toHaveLength(12);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
for (const method of CMS_CLIENT_METHODS) {
|
|
24
|
+
it(`RestClient implements ${method}()`, () => {
|
|
25
|
+
expect(typeof restClient[method]).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it(`XmdsClient implements ${method}()`, () => {
|
|
29
|
+
expect(typeof xmdsClient[method]).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('assertCmsClient passes for RestClient', () => {
|
|
34
|
+
expect(() => assertCmsClient(restClient, 'RestClient')).not.toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('assertCmsClient passes for XmdsClient', () => {
|
|
38
|
+
expect(() => assertCmsClient(xmdsClient, 'XmdsClient')).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('assertCmsClient throws for incomplete client', () => {
|
|
42
|
+
const partial = { registerDisplay: () => {} };
|
|
43
|
+
expect(() => assertCmsClient(partial, 'partial')).toThrow('partial missing CmsClient method');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('assertCmsClient throws for non-function property', () => {
|
|
47
|
+
const bad = Object.fromEntries(CMS_CLIENT_METHODS.map(m => [m, () => {}]));
|
|
48
|
+
bad.schedule = 'not a function';
|
|
49
|
+
expect(() => assertCmsClient(bad, 'bad')).toThrow('bad missing CmsClient method: schedule()');
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export const VERSION = pkg.version;
|
|
|
4
4
|
export { RestClient } from './rest-client.js';
|
|
5
5
|
export { XmdsClient } from './xmds-client.js';
|
|
6
6
|
export { ProtocolDetector } from './protocol-detector.js';
|
|
7
|
+
export { CMS_CLIENT_METHODS, assertCmsClient } from './cms-client.js';
|
|
7
8
|
export { parseScheduleResponse } from './schedule-parser.js';
|
package/src/protocol-detector.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* CMS Protocol Auto-Detector
|
|
3
3
|
*
|
|
4
4
|
* Probes the CMS to determine which communication protocol to use:
|
|
5
|
-
* - REST/
|
|
5
|
+
* - REST/PlayerRestApi — optimized JSON protocol (custom CMS image)
|
|
6
6
|
* - SOAP/XMDS — universal XML protocol (any vanilla Xibo CMS)
|
|
7
7
|
*
|
|
8
8
|
* Detection logic:
|
|
9
|
-
* 1. GET {cmsUrl}/
|
|
9
|
+
* 1. GET {cmsUrl}${PLAYER_API}/health with a 3-second timeout
|
|
10
10
|
* 2. If 200 + valid JSON → REST
|
|
11
11
|
* 3. If 404/error/timeout → SOAP (fallback)
|
|
12
12
|
*
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { createLogger } from '@xiboplayer/utils';
|
|
28
|
+
import { assertCmsClient } from './cms-client.js';
|
|
28
29
|
|
|
29
30
|
const log = createLogger('Protocol');
|
|
30
31
|
|
|
@@ -54,7 +55,7 @@ export class ProtocolDetector {
|
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Probe the CMS health endpoint to determine protocol availability.
|
|
57
|
-
* @returns {Promise<boolean>} true if REST/
|
|
58
|
+
* @returns {Promise<boolean>} true if REST/PlayerRestApi is available
|
|
58
59
|
*/
|
|
59
60
|
async probe() {
|
|
60
61
|
const available = await this.RestClient.isAvailable(this.cmsUrl, {
|
|
@@ -78,13 +79,17 @@ export class ProtocolDetector {
|
|
|
78
79
|
if (forceProtocol === 'rest') {
|
|
79
80
|
this.protocol = 'rest';
|
|
80
81
|
log.info('Using REST transport (forced)');
|
|
81
|
-
|
|
82
|
+
const client = new this.RestClient(config);
|
|
83
|
+
assertCmsClient(client, 'RestClient');
|
|
84
|
+
return { client, protocol: 'rest' };
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
if (forceProtocol === 'xmds') {
|
|
85
88
|
this.protocol = 'xmds';
|
|
86
89
|
log.info('Using XMDS/SOAP transport (forced)');
|
|
87
|
-
|
|
90
|
+
const client = new this.XmdsClient(config);
|
|
91
|
+
assertCmsClient(client, 'XmdsClient');
|
|
92
|
+
return { client, protocol: 'xmds' };
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
// Auto-detect
|
|
@@ -98,13 +103,17 @@ export class ProtocolDetector {
|
|
|
98
103
|
|
|
99
104
|
if (isRest) {
|
|
100
105
|
this.protocol = 'rest';
|
|
101
|
-
log.info('REST transport detected — using
|
|
102
|
-
|
|
106
|
+
log.info('REST transport detected — using PlayerRestApi');
|
|
107
|
+
const client = new this.RestClient(config);
|
|
108
|
+
assertCmsClient(client, 'RestClient');
|
|
109
|
+
return { client, protocol: 'rest' };
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
this.protocol = 'xmds';
|
|
106
113
|
log.info('REST unavailable — using XMDS/SOAP transport');
|
|
107
|
-
|
|
114
|
+
const client = new this.XmdsClient(config);
|
|
115
|
+
assertCmsClient(client, 'XmdsClient');
|
|
116
|
+
return { client, protocol: 'xmds' };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
/**
|
|
@@ -132,6 +141,7 @@ export class ProtocolDetector {
|
|
|
132
141
|
log.info(`Protocol changed: ${previousProtocol} → ${newProtocol}`);
|
|
133
142
|
this.protocol = newProtocol;
|
|
134
143
|
const client = isRest ? new this.RestClient(config) : new this.XmdsClient(config);
|
|
144
|
+
assertCmsClient(client, isRest ? 'RestClient' : 'XmdsClient');
|
|
135
145
|
return { client, protocol: newProtocol, changed: true };
|
|
136
146
|
}
|
|
137
147
|
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
12
12
|
import { ProtocolDetector } from './protocol-detector.js';
|
|
13
|
+
import { CMS_CLIENT_METHODS } from './cms-client.js';
|
|
14
|
+
|
|
15
|
+
// Stub all CmsClient methods on a mock constructor's prototype
|
|
16
|
+
function addCmsClientStubs(MockClass) {
|
|
17
|
+
for (const method of CMS_CLIENT_METHODS) {
|
|
18
|
+
MockClass.prototype[method] = vi.fn();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
describe('ProtocolDetector', () => {
|
|
15
23
|
let MockRestClient;
|
|
@@ -28,11 +36,13 @@ describe('ProtocolDetector', () => {
|
|
|
28
36
|
this.type = 'rest';
|
|
29
37
|
});
|
|
30
38
|
MockRestClient.isAvailable = vi.fn();
|
|
39
|
+
addCmsClientStubs(MockRestClient);
|
|
31
40
|
|
|
32
41
|
MockXmdsClient = vi.fn(function (cfg) {
|
|
33
42
|
this.config = cfg;
|
|
34
43
|
this.type = 'xmds';
|
|
35
44
|
});
|
|
45
|
+
addCmsClientStubs(MockXmdsClient);
|
|
36
46
|
});
|
|
37
47
|
|
|
38
48
|
// ── detect() ─────────────────────────────────────────────────────
|
package/src/rest-client.js
CHANGED
|
@@ -513,7 +513,7 @@ export class RestClient {
|
|
|
513
513
|
|
|
514
514
|
/**
|
|
515
515
|
* Probe whether the CMS supports API v2.
|
|
516
|
-
* GET /
|
|
516
|
+
* GET ${PLAYER_API}/health → { version: 2, status: "ok" }
|
|
517
517
|
*
|
|
518
518
|
* @param {string} cmsUrl - CMS base URL
|
|
519
519
|
* @param {Object} [retryOptions] - Retry options for fetch
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* with the Player API module deployed.
|
|
6
6
|
*
|
|
7
7
|
* Prerequisites:
|
|
8
|
-
* - CMS at CMS_URL must have
|
|
8
|
+
* - CMS at CMS_URL must have ${PLAYER_API}/* endpoints deployed
|
|
9
9
|
* - A display with the given HARDWARE_KEY must exist and be authorized
|
|
10
10
|
* - The SERVER_KEY must match the CMS setting
|
|
11
11
|
*
|