@xiboplayer/xmds 0.6.1 → 0.6.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 +2 -2
- package/src/index.d.ts +17 -0
- package/src/index.js +1 -0
- package/src/protocol-detector.js +149 -0
- package/src/protocol-detector.test.js +234 -0
- package/src/rest-client.js +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/xmds",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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.3"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0"
|
package/src/index.d.ts
CHANGED
|
@@ -36,4 +36,21 @@ export class XmdsClient {
|
|
|
36
36
|
mediaInventory(inventoryXml: string): Promise<boolean>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export class ProtocolDetector {
|
|
40
|
+
constructor(
|
|
41
|
+
cmsUrl: string,
|
|
42
|
+
RestClientClass: typeof RestClient,
|
|
43
|
+
XmdsClientClass: typeof XmdsClient,
|
|
44
|
+
options?: { probeTimeoutMs?: number }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
protocol: 'rest' | 'xmds' | null;
|
|
48
|
+
lastProbeTime: number;
|
|
49
|
+
|
|
50
|
+
probe(): Promise<boolean>;
|
|
51
|
+
detect(config: any, forceProtocol?: 'rest' | 'xmds'): Promise<{ client: any; protocol: 'rest' | 'xmds' }>;
|
|
52
|
+
reprobe(config: any): Promise<{ client: any; protocol: 'rest' | 'xmds'; changed: boolean }>;
|
|
53
|
+
getProtocol(): 'rest' | 'xmds' | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
export function parseScheduleResponse(data: any): any;
|
package/src/index.js
CHANGED
|
@@ -3,4 +3,5 @@ import pkg from '../package.json' with { type: 'json' };
|
|
|
3
3
|
export const VERSION = pkg.version;
|
|
4
4
|
export { RestClient } from './rest-client.js';
|
|
5
5
|
export { XmdsClient } from './xmds-client.js';
|
|
6
|
+
export { ProtocolDetector } from './protocol-detector.js';
|
|
6
7
|
export { parseScheduleResponse } from './schedule-parser.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMS Protocol Auto-Detector
|
|
3
|
+
*
|
|
4
|
+
* Probes the CMS to determine which communication protocol to use:
|
|
5
|
+
* - REST/PlayerApiV2 — optimized JSON protocol (custom CMS image)
|
|
6
|
+
* - SOAP/XMDS — universal XML protocol (any vanilla Xibo CMS)
|
|
7
|
+
*
|
|
8
|
+
* Detection logic:
|
|
9
|
+
* 1. GET {cmsUrl}/api/v2/player/health with a 3-second timeout
|
|
10
|
+
* 2. If 200 + valid JSON → REST
|
|
11
|
+
* 3. If 404/error/timeout → SOAP (fallback)
|
|
12
|
+
*
|
|
13
|
+
* The detected protocol is cached and can be re-probed on connection errors.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { ProtocolDetector } from '@xiboplayer/xmds';
|
|
17
|
+
* import { RestClient, XmdsClient } from '@xiboplayer/xmds';
|
|
18
|
+
*
|
|
19
|
+
* const detector = new ProtocolDetector(config.cmsUrl, RestClient, XmdsClient);
|
|
20
|
+
* const xmds = await detector.detect(config);
|
|
21
|
+
* // xmds is either a RestClient or XmdsClient instance
|
|
22
|
+
*
|
|
23
|
+
* // On connection errors, re-probe:
|
|
24
|
+
* const newXmds = await detector.reprobe(config);
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createLogger } from '@xiboplayer/utils';
|
|
28
|
+
|
|
29
|
+
const log = createLogger('Protocol');
|
|
30
|
+
|
|
31
|
+
/** Default probe timeout in milliseconds */
|
|
32
|
+
const PROBE_TIMEOUT_MS = 3000;
|
|
33
|
+
|
|
34
|
+
export class ProtocolDetector {
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} cmsUrl - CMS base URL
|
|
37
|
+
* @param {typeof import('./rest-client.js').RestClient} RestClientClass - RestClient constructor
|
|
38
|
+
* @param {typeof import('./xmds-client.js').XmdsClient} XmdsClientClass - XmdsClient constructor
|
|
39
|
+
* @param {Object} [options]
|
|
40
|
+
* @param {number} [options.probeTimeoutMs=3000] - Timeout for health probe
|
|
41
|
+
*/
|
|
42
|
+
constructor(cmsUrl, RestClientClass, XmdsClientClass, options = {}) {
|
|
43
|
+
this.cmsUrl = cmsUrl;
|
|
44
|
+
this.RestClient = RestClientClass;
|
|
45
|
+
this.XmdsClient = XmdsClientClass;
|
|
46
|
+
this.probeTimeoutMs = options.probeTimeoutMs || PROBE_TIMEOUT_MS;
|
|
47
|
+
|
|
48
|
+
/** @type {'rest'|'xmds'|null} Detected protocol (null = not yet probed) */
|
|
49
|
+
this.protocol = null;
|
|
50
|
+
|
|
51
|
+
/** @type {number} Timestamp of last successful probe */
|
|
52
|
+
this.lastProbeTime = 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Probe the CMS health endpoint to determine protocol availability.
|
|
57
|
+
* @returns {Promise<boolean>} true if REST/PlayerApiV2 is available
|
|
58
|
+
*/
|
|
59
|
+
async probe() {
|
|
60
|
+
const available = await this.RestClient.isAvailable(this.cmsUrl, {
|
|
61
|
+
maxRetries: 0,
|
|
62
|
+
timeoutMs: this.probeTimeoutMs,
|
|
63
|
+
});
|
|
64
|
+
this.lastProbeTime = Date.now();
|
|
65
|
+
return available;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Detect the best protocol and create the appropriate client.
|
|
70
|
+
* On first call, probes the CMS. On subsequent calls, returns the cached
|
|
71
|
+
* protocol unless reprobe() is called.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} config - Player configuration (passed to client constructor)
|
|
74
|
+
* @param {string} [forceProtocol] - 'rest'|'xmds' to skip detection
|
|
75
|
+
* @returns {Promise<{client: any, protocol: 'rest'|'xmds'}>}
|
|
76
|
+
*/
|
|
77
|
+
async detect(config, forceProtocol) {
|
|
78
|
+
if (forceProtocol === 'rest') {
|
|
79
|
+
this.protocol = 'rest';
|
|
80
|
+
log.info('Using REST transport (forced)');
|
|
81
|
+
return { client: new this.RestClient(config), protocol: 'rest' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (forceProtocol === 'xmds') {
|
|
85
|
+
this.protocol = 'xmds';
|
|
86
|
+
log.info('Using XMDS/SOAP transport (forced)');
|
|
87
|
+
return { client: new this.XmdsClient(config), protocol: 'xmds' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Auto-detect
|
|
91
|
+
log.info('Probing CMS for REST API availability...');
|
|
92
|
+
let isRest = false;
|
|
93
|
+
try {
|
|
94
|
+
isRest = await this.probe();
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log.warn('REST probe failed:', e?.message || e);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isRest) {
|
|
100
|
+
this.protocol = 'rest';
|
|
101
|
+
log.info('REST transport detected — using PlayerApiV2');
|
|
102
|
+
return { client: new this.RestClient(config), protocol: 'rest' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.protocol = 'xmds';
|
|
106
|
+
log.info('REST unavailable — using XMDS/SOAP transport');
|
|
107
|
+
return { client: new this.XmdsClient(config), protocol: 'xmds' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Re-probe the CMS and potentially switch protocols.
|
|
112
|
+
* Called on connection errors to check if the CMS was upgraded/downgraded.
|
|
113
|
+
*
|
|
114
|
+
* @param {Object} config - Player configuration
|
|
115
|
+
* @returns {Promise<{client: any, protocol: 'rest'|'xmds', changed: boolean}>}
|
|
116
|
+
*/
|
|
117
|
+
async reprobe(config) {
|
|
118
|
+
const previousProtocol = this.protocol;
|
|
119
|
+
|
|
120
|
+
log.info('Re-probing CMS protocol...');
|
|
121
|
+
let isRest = false;
|
|
122
|
+
try {
|
|
123
|
+
isRest = await this.probe();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
log.warn('Re-probe failed:', e?.message || e);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const newProtocol = isRest ? 'rest' : 'xmds';
|
|
129
|
+
const changed = newProtocol !== previousProtocol;
|
|
130
|
+
|
|
131
|
+
if (changed) {
|
|
132
|
+
log.info(`Protocol changed: ${previousProtocol} → ${newProtocol}`);
|
|
133
|
+
this.protocol = newProtocol;
|
|
134
|
+
const client = isRest ? new this.RestClient(config) : new this.XmdsClient(config);
|
|
135
|
+
return { client, protocol: newProtocol, changed: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log.info(`Protocol unchanged: ${newProtocol}`);
|
|
139
|
+
return { client: null, protocol: newProtocol, changed: false };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get the currently detected protocol.
|
|
144
|
+
* @returns {'rest'|'xmds'|null}
|
|
145
|
+
*/
|
|
146
|
+
getProtocol() {
|
|
147
|
+
return this.protocol;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProtocolDetector Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests CMS protocol auto-detection logic:
|
|
5
|
+
* - REST probing via RestClient.isAvailable()
|
|
6
|
+
* - Fallback to XMDS/SOAP when REST is unavailable
|
|
7
|
+
* - Forced protocol selection (bypass auto-detection)
|
|
8
|
+
* - Re-probing on connection errors
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
12
|
+
import { ProtocolDetector } from './protocol-detector.js';
|
|
13
|
+
|
|
14
|
+
describe('ProtocolDetector', () => {
|
|
15
|
+
let MockRestClient;
|
|
16
|
+
let MockXmdsClient;
|
|
17
|
+
let config;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
config = {
|
|
21
|
+
cmsUrl: 'https://cms.example.com',
|
|
22
|
+
cmsKey: 'test-key',
|
|
23
|
+
hardwareKey: 'test-hw',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
MockRestClient = vi.fn(function (cfg) {
|
|
27
|
+
this.config = cfg;
|
|
28
|
+
this.type = 'rest';
|
|
29
|
+
});
|
|
30
|
+
MockRestClient.isAvailable = vi.fn();
|
|
31
|
+
|
|
32
|
+
MockXmdsClient = vi.fn(function (cfg) {
|
|
33
|
+
this.config = cfg;
|
|
34
|
+
this.type = 'xmds';
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── detect() ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
describe('detect()', () => {
|
|
41
|
+
it('should detect REST when health endpoint returns 200', async () => {
|
|
42
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
43
|
+
|
|
44
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
45
|
+
const { client, protocol } = await detector.detect(config);
|
|
46
|
+
|
|
47
|
+
expect(protocol).toBe('rest');
|
|
48
|
+
expect(client.type).toBe('rest');
|
|
49
|
+
expect(detector.getProtocol()).toBe('rest');
|
|
50
|
+
expect(MockRestClient.isAvailable).toHaveBeenCalledWith('https://cms.example.com', {
|
|
51
|
+
maxRetries: 0,
|
|
52
|
+
timeoutMs: 3000,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should fall back to XMDS when REST is unavailable', async () => {
|
|
57
|
+
MockRestClient.isAvailable.mockResolvedValue(false);
|
|
58
|
+
|
|
59
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
60
|
+
const { client, protocol } = await detector.detect(config);
|
|
61
|
+
|
|
62
|
+
expect(protocol).toBe('xmds');
|
|
63
|
+
expect(client.type).toBe('xmds');
|
|
64
|
+
expect(detector.getProtocol()).toBe('xmds');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fall back to XMDS when probe throws', async () => {
|
|
68
|
+
MockRestClient.isAvailable.mockRejectedValue(new Error('Network error'));
|
|
69
|
+
|
|
70
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
71
|
+
// probe() will throw, but detect() should handle it
|
|
72
|
+
// Actually, isAvailable catches internally and returns false
|
|
73
|
+
// Let's test the direct throw case by mocking probe
|
|
74
|
+
const { client, protocol } = await detector.detect(config);
|
|
75
|
+
|
|
76
|
+
expect(protocol).toBe('xmds');
|
|
77
|
+
expect(client.type).toBe('xmds');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use forced REST protocol without probing', async () => {
|
|
81
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
82
|
+
const { client, protocol } = await detector.detect(config, 'rest');
|
|
83
|
+
|
|
84
|
+
expect(protocol).toBe('rest');
|
|
85
|
+
expect(client.type).toBe('rest');
|
|
86
|
+
expect(MockRestClient.isAvailable).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should use forced XMDS protocol without probing', async () => {
|
|
90
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
91
|
+
const { client, protocol } = await detector.detect(config, 'xmds');
|
|
92
|
+
|
|
93
|
+
expect(protocol).toBe('xmds');
|
|
94
|
+
expect(client.type).toBe('xmds');
|
|
95
|
+
expect(MockRestClient.isAvailable).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should pass config to client constructor', async () => {
|
|
99
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
100
|
+
|
|
101
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
102
|
+
const { client } = await detector.detect(config);
|
|
103
|
+
|
|
104
|
+
expect(MockRestClient).toHaveBeenCalledWith(config);
|
|
105
|
+
expect(client.config).toBe(config);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should use custom probe timeout', async () => {
|
|
109
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
110
|
+
|
|
111
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient, {
|
|
112
|
+
probeTimeoutMs: 5000,
|
|
113
|
+
});
|
|
114
|
+
await detector.detect(config);
|
|
115
|
+
|
|
116
|
+
expect(MockRestClient.isAvailable).toHaveBeenCalledWith('https://cms.example.com', {
|
|
117
|
+
maxRetries: 0,
|
|
118
|
+
timeoutMs: 5000,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should record lastProbeTime after detection', async () => {
|
|
123
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
124
|
+
|
|
125
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
126
|
+
expect(detector.lastProbeTime).toBe(0);
|
|
127
|
+
|
|
128
|
+
await detector.detect(config);
|
|
129
|
+
|
|
130
|
+
expect(detector.lastProbeTime).toBeGreaterThan(0);
|
|
131
|
+
expect(detector.lastProbeTime).toBeLessThanOrEqual(Date.now());
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── reprobe() ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
describe('reprobe()', () => {
|
|
138
|
+
it('should detect protocol change from XMDS to REST', async () => {
|
|
139
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(false); // initial detect
|
|
140
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
141
|
+
await detector.detect(config);
|
|
142
|
+
expect(detector.getProtocol()).toBe('xmds');
|
|
143
|
+
|
|
144
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(true); // reprobe
|
|
145
|
+
const { client, protocol, changed } = await detector.reprobe(config);
|
|
146
|
+
|
|
147
|
+
expect(changed).toBe(true);
|
|
148
|
+
expect(protocol).toBe('rest');
|
|
149
|
+
expect(client.type).toBe('rest');
|
|
150
|
+
expect(detector.getProtocol()).toBe('rest');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should detect protocol change from REST to XMDS', async () => {
|
|
154
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(true); // initial detect
|
|
155
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
156
|
+
await detector.detect(config);
|
|
157
|
+
expect(detector.getProtocol()).toBe('rest');
|
|
158
|
+
|
|
159
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(false); // reprobe
|
|
160
|
+
const { client, protocol, changed } = await detector.reprobe(config);
|
|
161
|
+
|
|
162
|
+
expect(changed).toBe(true);
|
|
163
|
+
expect(protocol).toBe('xmds');
|
|
164
|
+
expect(client.type).toBe('xmds');
|
|
165
|
+
expect(detector.getProtocol()).toBe('xmds');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return changed=false when protocol is unchanged', async () => {
|
|
169
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(true); // initial detect
|
|
170
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
171
|
+
await detector.detect(config);
|
|
172
|
+
|
|
173
|
+
MockRestClient.isAvailable.mockResolvedValueOnce(true); // reprobe (same)
|
|
174
|
+
const { client, protocol, changed } = await detector.reprobe(config);
|
|
175
|
+
|
|
176
|
+
expect(changed).toBe(false);
|
|
177
|
+
expect(protocol).toBe('rest');
|
|
178
|
+
expect(client).toBeNull(); // No new client when unchanged
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should update lastProbeTime on reprobe', async () => {
|
|
182
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
183
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
184
|
+
await detector.detect(config);
|
|
185
|
+
const firstProbe = detector.lastProbeTime;
|
|
186
|
+
|
|
187
|
+
// Small delay to ensure different timestamp
|
|
188
|
+
await new Promise(r => setTimeout(r, 5));
|
|
189
|
+
|
|
190
|
+
await detector.reprobe(config);
|
|
191
|
+
expect(detector.lastProbeTime).toBeGreaterThanOrEqual(firstProbe);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── getProtocol() ────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
describe('getProtocol()', () => {
|
|
198
|
+
it('should return null before detection', () => {
|
|
199
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
200
|
+
expect(detector.getProtocol()).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should return detected protocol after detect()', async () => {
|
|
204
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
205
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
206
|
+
await detector.detect(config);
|
|
207
|
+
expect(detector.getProtocol()).toBe('rest');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ── probe() ──────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
describe('probe()', () => {
|
|
214
|
+
it('should call RestClient.isAvailable with correct URL and timeout', async () => {
|
|
215
|
+
MockRestClient.isAvailable.mockResolvedValue(true);
|
|
216
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
217
|
+
const result = await detector.probe();
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(true);
|
|
220
|
+
expect(MockRestClient.isAvailable).toHaveBeenCalledWith('https://cms.example.com', {
|
|
221
|
+
maxRetries: 0,
|
|
222
|
+
timeoutMs: 3000,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should return false when REST is unavailable', async () => {
|
|
227
|
+
MockRestClient.isAvailable.mockResolvedValue(false);
|
|
228
|
+
const detector = new ProtocolDetector('https://cms.example.com', MockRestClient, MockXmdsClient);
|
|
229
|
+
const result = await detector.probe();
|
|
230
|
+
|
|
231
|
+
expect(result).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
package/src/rest-client.js
CHANGED
|
@@ -526,7 +526,13 @@ export class RestClient {
|
|
|
526
526
|
(window.electronAPI?.isElectron || window.location.hostname === 'localhost');
|
|
527
527
|
const base = isProxy ? '' : cmsUrl.replace(/\/+$/, '');
|
|
528
528
|
const url = `${base}${PLAYER_API}/health`;
|
|
529
|
-
const
|
|
529
|
+
const timeoutMs = retryOptions?.timeoutMs || 3000;
|
|
530
|
+
const fetchOptions = { method: 'GET' };
|
|
531
|
+
// Apply timeout via AbortSignal (short timeout avoids delaying startup)
|
|
532
|
+
if (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) {
|
|
533
|
+
fetchOptions.signal = AbortSignal.timeout(timeoutMs);
|
|
534
|
+
}
|
|
535
|
+
const response = await fetchWithRetry(url, fetchOptions, retryOptions || { maxRetries: 0 });
|
|
530
536
|
if (!response.ok) return false;
|
|
531
537
|
const data = await response.json();
|
|
532
538
|
return data.version === 2 && data.status === 'ok';
|