retold-remote 0.0.23 → 0.0.26
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/css/retold-remote.css +343 -20
- package/docs/.nojekyll +0 -0
- package/docs/README.md +64 -12
- package/docs/_cover.md +6 -6
- package/docs/_sidebar.md +2 -0
- package/docs/_topbar.md +1 -1
- package/docs/_version.json +7 -0
- package/docs/collections.md +30 -0
- package/docs/css/docuserve.css +327 -0
- package/docs/ebook-reader.md +75 -1
- package/docs/image-explorer.md +62 -2
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +254 -0
- package/docs/retold-keyword-index.json +31216 -0
- package/docs/server-setup.md +122 -91
- package/docs/stack-launcher.md +218 -0
- package/docs/synology.md +585 -0
- package/docs/ultravisor-configuration.md +5 -5
- package/docs/ultravisor-integration.md +4 -2
- package/package.json +20 -14
- package/source/Pict-Application-RetoldRemote.js +22 -0
- package/source/RetoldRemote-ExtensionMaps.js +1 -1
- package/source/cli/RetoldRemote-Server-Setup.js +460 -7
- package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
- package/source/cli/RetoldRemote-Stack-Run.js +41 -0
- package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
- package/source/providers/CollectionManager-AddItems.js +166 -0
- package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
- package/source/providers/Pict-Provider-OperationStatus.js +597 -0
- package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
- package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
- package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
- package/source/server/RetoldRemote-CollectionExportService.js +763 -0
- package/source/server/RetoldRemote-CollectionService.js +5 -0
- package/source/server/RetoldRemote-EbookService.js +218 -3
- package/source/server/RetoldRemote-ImageService.js +221 -46
- package/source/server/RetoldRemote-MediaService.js +63 -4
- package/source/server/RetoldRemote-MetadataCache.js +25 -5
- package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
- package/source/server/RetoldRemote-SubimageService.js +680 -0
- package/source/server/RetoldRemote-ToolDetector.js +50 -0
- package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
- package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
- package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
- package/source/server/RetoldRemote-VideoFrameService.js +302 -9
- package/source/views/MediaViewer-EbookViewer.js +419 -1
- package/source/views/MediaViewer-PdfViewer.js +1050 -0
- package/source/views/PictView-Remote-AudioExplorer.js +77 -1
- package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
- package/source/views/PictView-Remote-Gallery.js +365 -64
- package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
- package/source/views/PictView-Remote-ImageViewer.js +2 -2
- package/source/views/PictView-Remote-Layout.js +58 -0
- package/source/views/PictView-Remote-MediaViewer.js +100 -25
- package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
- package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
- package/source/views/PictView-Remote-TopBar.js +1 -0
- package/source/views/PictView-Remote-VideoExplorer.js +77 -1
- package/web-application/css/docuserve.css +277 -23
- package/web-application/css/retold-remote.css +343 -20
- package/web-application/docs/README.md +64 -12
- package/web-application/docs/_cover.md +6 -6
- package/web-application/docs/_sidebar.md +2 -0
- package/web-application/docs/_topbar.md +1 -1
- package/web-application/docs/collections.md +30 -0
- package/web-application/docs/ebook-reader.md +75 -1
- package/web-application/docs/image-explorer.md +62 -2
- package/web-application/docs/server-setup.md +122 -91
- package/web-application/docs/stack-launcher.md +218 -0
- package/web-application/docs/synology.md +585 -0
- package/web-application/docs/ultravisor-configuration.md +5 -5
- package/web-application/docs/ultravisor-integration.md +4 -2
- package/web-application/js/pict-docuserve.min.js +12 -12
- package/web-application/js/pict.min.js +2 -2
- package/web-application/js/pict.min.js.map +1 -1
- package/web-application/retold-remote.js +6596 -1784
- package/web-application/retold-remote.js.map +1 -1
- package/web-application/retold-remote.min.js +75 -23
- package/web-application/retold-remote.min.js.map +1 -1
|
@@ -12,11 +12,14 @@
|
|
|
12
12
|
* file resolution. Results are returned as binary (OutputBuffer) or
|
|
13
13
|
* JSON (TaskOutputs) depending on the operation type.
|
|
14
14
|
*
|
|
15
|
+
* HTTP transport is delegated to `fable-ultravisor-client`. This service
|
|
16
|
+
* owns retold-remote-specific concerns: UltravisorURL gating, periodic
|
|
17
|
+
* state refresh, capability/beacon-count tracking, and ContentAPIURL.
|
|
18
|
+
*
|
|
15
19
|
* @license MIT
|
|
16
20
|
*/
|
|
17
21
|
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
18
|
-
const
|
|
19
|
-
const libHTTPS = require('https');
|
|
22
|
+
const libFableUltravisorClient = require('fable-ultravisor-client');
|
|
20
23
|
|
|
21
24
|
class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
22
25
|
{
|
|
@@ -38,8 +41,19 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
38
41
|
this._BeaconCount = 0;
|
|
39
42
|
this._Capabilities = [];
|
|
40
43
|
|
|
41
|
-
//
|
|
42
|
-
this.
|
|
44
|
+
// Credentials for the service account retold-remote authenticates as
|
|
45
|
+
let tmpUserName = this.fable.settings.UltravisorUserName || 'retold-remote-dispatcher';
|
|
46
|
+
let tmpPassword = (typeof(this.fable.settings.UltravisorPassword) === 'string')
|
|
47
|
+
? this.fable.settings.UltravisorPassword
|
|
48
|
+
: '';
|
|
49
|
+
|
|
50
|
+
// Underlying HTTP client (owns session cookie, frame parser, etc.)
|
|
51
|
+
this._Client = new libFableUltravisorClient(this.fable,
|
|
52
|
+
{
|
|
53
|
+
UltravisorURL: this._UltravisorURL,
|
|
54
|
+
UserName: tmpUserName,
|
|
55
|
+
Password: tmpPassword
|
|
56
|
+
});
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -71,7 +85,7 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
71
85
|
this.fable.log.info(`Ultravisor Dispatcher: checking connection to ${this._UltravisorURL}`);
|
|
72
86
|
|
|
73
87
|
// Authenticate first, then refresh state
|
|
74
|
-
this.
|
|
88
|
+
this._Client.authenticate((pAuthError) =>
|
|
75
89
|
{
|
|
76
90
|
if (pAuthError)
|
|
77
91
|
{
|
|
@@ -100,73 +114,6 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
100
114
|
});
|
|
101
115
|
}
|
|
102
116
|
|
|
103
|
-
/**
|
|
104
|
-
* Authenticate with the Ultravisor server to obtain a session cookie.
|
|
105
|
-
*
|
|
106
|
-
* @param {function} fCallback - function(pError)
|
|
107
|
-
*/
|
|
108
|
-
_authenticate(fCallback)
|
|
109
|
-
{
|
|
110
|
-
let tmpSelf = this;
|
|
111
|
-
let tmpBody = {
|
|
112
|
-
UserName: 'retold-remote-dispatcher',
|
|
113
|
-
Password: ''
|
|
114
|
-
};
|
|
115
|
-
let tmpBodyString = JSON.stringify(tmpBody);
|
|
116
|
-
let tmpParsedURL;
|
|
117
|
-
|
|
118
|
-
try
|
|
119
|
-
{
|
|
120
|
-
tmpParsedURL = new URL(this._UltravisorURL);
|
|
121
|
-
}
|
|
122
|
-
catch (pError)
|
|
123
|
-
{
|
|
124
|
-
return fCallback(new Error('Invalid UltravisorURL: ' + this._UltravisorURL));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let tmpLib = tmpParsedURL.protocol === 'https:' ? libHTTPS : libHTTP;
|
|
128
|
-
|
|
129
|
-
let tmpOptions = {
|
|
130
|
-
hostname: tmpParsedURL.hostname,
|
|
131
|
-
port: tmpParsedURL.port || (tmpParsedURL.protocol === 'https:' ? 443 : 80),
|
|
132
|
-
path: '/1.0/Authenticate',
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
'Content-Length': Buffer.byteLength(tmpBodyString)
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
let tmpReq = tmpLib.request(tmpOptions, (pResponse) =>
|
|
141
|
-
{
|
|
142
|
-
let tmpData = '';
|
|
143
|
-
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
144
|
-
pResponse.on('end', () =>
|
|
145
|
-
{
|
|
146
|
-
if (pResponse.statusCode >= 400)
|
|
147
|
-
{
|
|
148
|
-
return fCallback(new Error(`Authentication failed: HTTP ${pResponse.statusCode}`));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Extract session cookie from Set-Cookie headers
|
|
152
|
-
let tmpSetCookieHeaders = pResponse.headers['set-cookie'];
|
|
153
|
-
if (tmpSetCookieHeaders && tmpSetCookieHeaders.length > 0)
|
|
154
|
-
{
|
|
155
|
-
let tmpCookieParts = tmpSetCookieHeaders[0].split(';');
|
|
156
|
-
tmpSelf._SessionCookie = tmpCookieParts[0].trim();
|
|
157
|
-
tmpSelf.fable.log.info('Ultravisor Dispatcher: authenticated.');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return fCallback(null);
|
|
161
|
-
});
|
|
162
|
-
pResponse.on('error', fCallback);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
tmpReq.on('error', fCallback);
|
|
166
|
-
tmpReq.write(tmpBodyString);
|
|
167
|
-
tmpReq.end();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
117
|
/**
|
|
171
118
|
* Refresh cached Ultravisor state (capabilities, beacon count).
|
|
172
119
|
*
|
|
@@ -176,45 +123,44 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
176
123
|
{
|
|
177
124
|
let tmpSelf = this;
|
|
178
125
|
|
|
179
|
-
this.
|
|
180
|
-
|
|
126
|
+
this._Client.getStatus((pError, pResult) =>
|
|
127
|
+
{
|
|
128
|
+
if (pError)
|
|
181
129
|
{
|
|
182
|
-
if (
|
|
130
|
+
if (tmpSelf._Available)
|
|
183
131
|
{
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
tmpSelf.fable.log.warn(`Ultravisor Dispatcher: connection lost — ${pError.message}. Processing will be local.`);
|
|
187
|
-
}
|
|
188
|
-
tmpSelf._Available = false;
|
|
189
|
-
tmpSelf._BeaconCount = 0;
|
|
190
|
-
tmpSelf._Capabilities = [];
|
|
191
|
-
if (fCallback) return fCallback(null);
|
|
192
|
-
return;
|
|
132
|
+
tmpSelf.fable.log.warn(`Ultravisor Dispatcher: connection lost — ${pError.message}. Processing will be local.`);
|
|
193
133
|
}
|
|
134
|
+
tmpSelf._Available = false;
|
|
135
|
+
tmpSelf._BeaconCount = 0;
|
|
136
|
+
tmpSelf._Capabilities = [];
|
|
137
|
+
if (fCallback) { return fCallback(null); }
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
194
140
|
|
|
195
|
-
|
|
196
|
-
|
|
141
|
+
let tmpPrevBeaconCount = tmpSelf._BeaconCount;
|
|
142
|
+
let tmpPrevAvailable = tmpSelf._Available;
|
|
197
143
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
144
|
+
tmpSelf._Capabilities = pResult.Capabilities || [];
|
|
145
|
+
tmpSelf._BeaconCount = pResult.BeaconCount || 0;
|
|
146
|
+
tmpSelf._Available = true;
|
|
201
147
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
148
|
+
// Log state transitions
|
|
149
|
+
if (!tmpPrevAvailable && tmpSelf._BeaconCount > 0)
|
|
150
|
+
{
|
|
151
|
+
tmpSelf.fable.log.info(`Ultravisor Dispatcher: connected — ${tmpSelf._BeaconCount} beacon(s), capabilities: [${tmpSelf._Capabilities.join(', ')}]`);
|
|
152
|
+
}
|
|
153
|
+
else if (tmpPrevBeaconCount === 0 && tmpSelf._BeaconCount > 0)
|
|
154
|
+
{
|
|
155
|
+
tmpSelf.fable.log.info(`Ultravisor Dispatcher: ${tmpSelf._BeaconCount} beacon(s) now available, capabilities: [${tmpSelf._Capabilities.join(', ')}]`);
|
|
156
|
+
}
|
|
157
|
+
else if (tmpSelf._BeaconCount === 0 && !tmpPrevAvailable)
|
|
158
|
+
{
|
|
159
|
+
tmpSelf.fable.log.warn('Ultravisor Dispatcher: connected but no beacons registered. Processing will be local until beacons connect.');
|
|
160
|
+
}
|
|
215
161
|
|
|
216
|
-
|
|
217
|
-
|
|
162
|
+
if (fCallback) { return fCallback(null); }
|
|
163
|
+
});
|
|
218
164
|
}
|
|
219
165
|
|
|
220
166
|
// ================================================================
|
|
@@ -241,123 +187,23 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
241
187
|
|
|
242
188
|
this.fable.log.info(`[TriggerOp] START triggerOperation("${pOperationHash}") params: ${JSON.stringify(pParameters)}`);
|
|
243
189
|
|
|
244
|
-
|
|
245
|
-
Parameters: pParameters || {},
|
|
246
|
-
Async: false,
|
|
247
|
-
TimeoutMs: (pParameters && pParameters.TimeoutMs) || 300000
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
let tmpParsedURL;
|
|
251
|
-
try
|
|
190
|
+
this._Client.triggerOperation(pOperationHash, pParameters, (pError, pResult) =>
|
|
252
191
|
{
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
catch (pURLError)
|
|
256
|
-
{
|
|
257
|
-
return fCallback(new Error('Invalid UltravisorURL: ' + this._UltravisorURL));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let tmpLib = tmpParsedURL.protocol === 'https:' ? libHTTPS : libHTTP;
|
|
261
|
-
|
|
262
|
-
let tmpHeaders = {
|
|
263
|
-
'Content-Type': 'application/json',
|
|
264
|
-
'Content-Length': Buffer.byteLength(tmpBody),
|
|
265
|
-
'Connection': 'keep-alive'
|
|
266
|
-
};
|
|
267
|
-
if (this._SessionCookie)
|
|
268
|
-
{
|
|
269
|
-
tmpHeaders['Cookie'] = this._SessionCookie;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let tmpOptions = {
|
|
273
|
-
hostname: tmpParsedURL.hostname,
|
|
274
|
-
port: tmpParsedURL.port || (tmpParsedURL.protocol === 'https:' ? 443 : 80),
|
|
275
|
-
path: '/Operation/' + encodeURIComponent(pOperationHash) + '/Trigger',
|
|
276
|
-
method: 'POST',
|
|
277
|
-
headers: tmpHeaders
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
let tmpCallbackFired = false;
|
|
281
|
-
let tmpComplete = (pError, pResult) =>
|
|
282
|
-
{
|
|
283
|
-
if (tmpCallbackFired) return;
|
|
284
|
-
tmpCallbackFired = true;
|
|
285
|
-
return fCallback(pError, pResult);
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
this.fable.log.info(`[TriggerOp] Sending POST ${tmpOptions.path} to ${tmpOptions.hostname}:${tmpOptions.port}`);
|
|
289
|
-
|
|
290
|
-
let tmpReq = tmpLib.request(tmpOptions, (pResponse) =>
|
|
291
|
-
{
|
|
292
|
-
let tmpContentType = pResponse.headers['content-type'] || '';
|
|
293
|
-
|
|
294
|
-
this.fable.log.info(`[TriggerOp] Response received: HTTP ${pResponse.statusCode} content-type="${tmpContentType}"`);
|
|
295
|
-
|
|
296
|
-
if (tmpContentType.indexOf('application/octet-stream') >= 0)
|
|
192
|
+
if (pError)
|
|
297
193
|
{
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.fable.log.info(`[TriggerOp] Binary response: ${tmpBuffer.length} bytes, run=${pResponse.headers['x-run-hash']}, status=${pResponse.headers['x-status']}`);
|
|
305
|
-
let tmpResult = {
|
|
306
|
-
Success: true,
|
|
307
|
-
OutputBuffer: tmpBuffer,
|
|
308
|
-
RunHash: pResponse.headers['x-run-hash'] || '',
|
|
309
|
-
Status: pResponse.headers['x-status'] || 'Complete',
|
|
310
|
-
ElapsedMs: parseInt(pResponse.headers['x-elapsed-ms'] || '0', 10)
|
|
311
|
-
};
|
|
312
|
-
return tmpComplete(null, tmpResult);
|
|
313
|
-
});
|
|
314
|
-
pResponse.on('error', tmpComplete);
|
|
194
|
+
this.fable.log.warn(`[TriggerOp] error: ${pError.message}`);
|
|
195
|
+
return fCallback(pError);
|
|
196
|
+
}
|
|
197
|
+
if (pResult && pResult.OutputBuffer)
|
|
198
|
+
{
|
|
199
|
+
this.fable.log.info(`[TriggerOp] Binary response: ${pResult.OutputBuffer.length} bytes, run=${pResult.RunHash}, status=${pResult.Status}`);
|
|
315
200
|
}
|
|
316
201
|
else
|
|
317
202
|
{
|
|
318
|
-
|
|
319
|
-
let tmpData = '';
|
|
320
|
-
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
321
|
-
pResponse.on('end', () =>
|
|
322
|
-
{
|
|
323
|
-
this.fable.log.info(`[TriggerOp] JSON response body: ${tmpData.substring(0, 500)}`);
|
|
324
|
-
try
|
|
325
|
-
{
|
|
326
|
-
let tmpParsed = JSON.parse(tmpData);
|
|
327
|
-
if (pResponse.statusCode >= 400)
|
|
328
|
-
{
|
|
329
|
-
this.fable.log.warn(`[TriggerOp] HTTP error ${pResponse.statusCode}: ${tmpParsed.Error || 'unknown'}`);
|
|
330
|
-
return tmpComplete(new Error(tmpParsed.Error || 'HTTP ' + pResponse.statusCode));
|
|
331
|
-
}
|
|
332
|
-
if (!tmpParsed.Success)
|
|
333
|
-
{
|
|
334
|
-
this.fable.log.warn(`[TriggerOp] Operation not successful. Errors: ${JSON.stringify(tmpParsed.Errors || [])}`);
|
|
335
|
-
return tmpComplete(new Error(
|
|
336
|
-
tmpParsed.Errors && tmpParsed.Errors.length > 0
|
|
337
|
-
? tmpParsed.Errors[0]
|
|
338
|
-
: 'Operation trigger failed'));
|
|
339
|
-
}
|
|
340
|
-
this.fable.log.info(`[TriggerOp] JSON success: run=${tmpParsed.RunHash}, status=${tmpParsed.Status}, keys=${Object.keys(tmpParsed).join(',')}`);
|
|
341
|
-
return tmpComplete(null, tmpParsed);
|
|
342
|
-
}
|
|
343
|
-
catch (pParseError)
|
|
344
|
-
{
|
|
345
|
-
this.fable.log.warn(`[TriggerOp] Failed to parse JSON response: ${pParseError.message}`);
|
|
346
|
-
return tmpComplete(new Error('Invalid response from trigger'));
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
pResponse.on('error', tmpComplete);
|
|
203
|
+
this.fable.log.info(`[TriggerOp] JSON success: run=${pResult && pResult.RunHash}, status=${pResult && pResult.Status}`);
|
|
350
204
|
}
|
|
205
|
+
return fCallback(null, pResult);
|
|
351
206
|
});
|
|
352
|
-
|
|
353
|
-
tmpReq.on('error', (pReqError) =>
|
|
354
|
-
{
|
|
355
|
-
this.fable.log.warn(`[TriggerOp] Request error: ${pReqError.message}`);
|
|
356
|
-
tmpComplete(pReqError);
|
|
357
|
-
});
|
|
358
|
-
tmpReq.setTimeout(0);
|
|
359
|
-
tmpReq.write(tmpBody);
|
|
360
|
-
tmpReq.end();
|
|
361
207
|
}
|
|
362
208
|
|
|
363
209
|
// ================================================================
|
|
@@ -372,23 +218,11 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
372
218
|
* enables real-time progress updates and efficient binary output
|
|
373
219
|
* transfer without base64 re-encoding overhead.
|
|
374
220
|
*
|
|
375
|
-
* Frame protocol
|
|
376
|
-
* [1 byte type][4 bytes payload length (uint32 big-endian)][payload]
|
|
377
|
-
* Type 0x01: Progress (JSON: { Percent, Message, Step, TotalSteps })
|
|
378
|
-
* Type 0x02: Intermediate (raw binary: e.g. thumbnail preview)
|
|
379
|
-
* Type 0x03: Final output (raw binary: completed file)
|
|
380
|
-
* Type 0x04: Result (JSON: { Success, Outputs, Log })
|
|
381
|
-
* Type 0x05: Error (JSON: { Error })
|
|
221
|
+
* Frame protocol is documented in fable-ultravisor-client.
|
|
382
222
|
*
|
|
383
|
-
* @param {object} pWorkItem - Work item details
|
|
384
|
-
* @param {object} pCallbacks - Event callbacks
|
|
385
|
-
*
|
|
386
|
-
* onProgress: function({ Percent, Message, Step, TotalSteps }) — optional
|
|
387
|
-
* onBinaryData: function(Buffer) — optional, intermediate binary data
|
|
388
|
-
* onError: function({ Error }) — optional, non-fatal error notification
|
|
389
|
-
* }
|
|
390
|
-
* @param {function} fCallback - function(pError, pResult) called on completion.
|
|
391
|
-
* pResult includes OutputBuffer (Buffer) if final binary output was streamed.
|
|
223
|
+
* @param {object} pWorkItem - Work item details
|
|
224
|
+
* @param {object} pCallbacks - Event callbacks ({ onProgress, onBinaryData, onError })
|
|
225
|
+
* @param {function} fCallback - function(pError, pResult)
|
|
392
226
|
*/
|
|
393
227
|
dispatchStream(pWorkItem, pCallbacks, fCallback)
|
|
394
228
|
{
|
|
@@ -397,267 +231,7 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
|
|
|
397
231
|
return fCallback(new Error('Ultravisor Dispatcher: not configured'));
|
|
398
232
|
}
|
|
399
233
|
|
|
400
|
-
|
|
401
|
-
try
|
|
402
|
-
{
|
|
403
|
-
tmpParsedURL = new URL(this._UltravisorURL);
|
|
404
|
-
}
|
|
405
|
-
catch (pError)
|
|
406
|
-
{
|
|
407
|
-
return fCallback(new Error('Invalid UltravisorURL: ' + this._UltravisorURL));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
let tmpLib = tmpParsedURL.protocol === 'https:' ? libHTTPS : libHTTP;
|
|
411
|
-
|
|
412
|
-
let tmpStreamHeaders = {
|
|
413
|
-
'Content-Type': 'application/json',
|
|
414
|
-
'Connection': 'keep-alive'
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
// Attach session cookie if available
|
|
418
|
-
if (this._SessionCookie)
|
|
419
|
-
{
|
|
420
|
-
tmpStreamHeaders['Cookie'] = this._SessionCookie;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
let tmpOptions = {
|
|
424
|
-
hostname: tmpParsedURL.hostname,
|
|
425
|
-
port: tmpParsedURL.port || (tmpParsedURL.protocol === 'https:' ? 443 : 80),
|
|
426
|
-
path: '/Beacon/Work/DispatchStream',
|
|
427
|
-
method: 'POST',
|
|
428
|
-
headers: tmpStreamHeaders
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
let tmpCallbackFired = false;
|
|
432
|
-
let tmpComplete = (pError, pResult) =>
|
|
433
|
-
{
|
|
434
|
-
if (tmpCallbackFired) { return; }
|
|
435
|
-
tmpCallbackFired = true;
|
|
436
|
-
fCallback(pError, pResult);
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
let tmpReq = tmpLib.request(tmpOptions, (pResponse) =>
|
|
440
|
-
{
|
|
441
|
-
// Non-streaming error response (4xx/5xx before stream starts)
|
|
442
|
-
if (pResponse.statusCode >= 400)
|
|
443
|
-
{
|
|
444
|
-
let tmpData = '';
|
|
445
|
-
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
446
|
-
pResponse.on('end', () =>
|
|
447
|
-
{
|
|
448
|
-
try
|
|
449
|
-
{
|
|
450
|
-
let tmpParsed = JSON.parse(tmpData);
|
|
451
|
-
tmpComplete(new Error(tmpParsed.Error || `HTTP ${pResponse.statusCode}`));
|
|
452
|
-
}
|
|
453
|
-
catch (pParseError)
|
|
454
|
-
{
|
|
455
|
-
tmpComplete(new Error(`HTTP ${pResponse.statusCode}: ${tmpData.substring(0, 200)}`));
|
|
456
|
-
}
|
|
457
|
-
});
|
|
458
|
-
pResponse.on('error', tmpComplete);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Binary frame stream parser
|
|
463
|
-
let tmpBuffer = Buffer.alloc(0);
|
|
464
|
-
let tmpLastResult = null;
|
|
465
|
-
let tmpBinaryChunks = [];
|
|
466
|
-
|
|
467
|
-
pResponse.on('data', (pChunk) =>
|
|
468
|
-
{
|
|
469
|
-
tmpBuffer = Buffer.concat([tmpBuffer, pChunk]);
|
|
470
|
-
|
|
471
|
-
// Parse complete frames from the buffer
|
|
472
|
-
while (tmpBuffer.length >= 5)
|
|
473
|
-
{
|
|
474
|
-
let tmpPayloadLen = tmpBuffer.readUInt32BE(1);
|
|
475
|
-
|
|
476
|
-
if (tmpBuffer.length < 5 + tmpPayloadLen)
|
|
477
|
-
{
|
|
478
|
-
break; // Need more data for this frame
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
let tmpType = tmpBuffer.readUInt8(0);
|
|
482
|
-
let tmpPayload = tmpBuffer.slice(5, 5 + tmpPayloadLen);
|
|
483
|
-
tmpBuffer = tmpBuffer.slice(5 + tmpPayloadLen);
|
|
484
|
-
|
|
485
|
-
switch (tmpType)
|
|
486
|
-
{
|
|
487
|
-
case 0x01: // Progress
|
|
488
|
-
if (pCallbacks && pCallbacks.onProgress)
|
|
489
|
-
{
|
|
490
|
-
try
|
|
491
|
-
{
|
|
492
|
-
pCallbacks.onProgress(JSON.parse(tmpPayload.toString()));
|
|
493
|
-
}
|
|
494
|
-
catch (pParseError)
|
|
495
|
-
{
|
|
496
|
-
// Ignore malformed progress frames
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
break;
|
|
500
|
-
|
|
501
|
-
case 0x02: // Intermediate binary data
|
|
502
|
-
if (pCallbacks && pCallbacks.onBinaryData)
|
|
503
|
-
{
|
|
504
|
-
pCallbacks.onBinaryData(Buffer.from(tmpPayload));
|
|
505
|
-
}
|
|
506
|
-
break;
|
|
507
|
-
|
|
508
|
-
case 0x03: // Final binary output
|
|
509
|
-
tmpBinaryChunks.push(Buffer.from(tmpPayload));
|
|
510
|
-
break;
|
|
511
|
-
|
|
512
|
-
case 0x04: // Result metadata
|
|
513
|
-
try
|
|
514
|
-
{
|
|
515
|
-
tmpLastResult = JSON.parse(tmpPayload.toString());
|
|
516
|
-
}
|
|
517
|
-
catch (pParseError)
|
|
518
|
-
{
|
|
519
|
-
// Ignore malformed result frames
|
|
520
|
-
}
|
|
521
|
-
break;
|
|
522
|
-
|
|
523
|
-
case 0x05: // Error
|
|
524
|
-
if (pCallbacks && pCallbacks.onError)
|
|
525
|
-
{
|
|
526
|
-
try
|
|
527
|
-
{
|
|
528
|
-
pCallbacks.onError(JSON.parse(tmpPayload.toString()));
|
|
529
|
-
}
|
|
530
|
-
catch (pParseError)
|
|
531
|
-
{
|
|
532
|
-
// Ignore malformed error frames
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
pResponse.on('end', () =>
|
|
541
|
-
{
|
|
542
|
-
if (tmpLastResult)
|
|
543
|
-
{
|
|
544
|
-
// Attach final binary output as a Buffer
|
|
545
|
-
if (tmpBinaryChunks.length > 0)
|
|
546
|
-
{
|
|
547
|
-
tmpLastResult.OutputBuffer = Buffer.concat(tmpBinaryChunks);
|
|
548
|
-
}
|
|
549
|
-
tmpComplete(null, tmpLastResult);
|
|
550
|
-
}
|
|
551
|
-
else
|
|
552
|
-
{
|
|
553
|
-
tmpComplete(new Error('Stream ended without result frame'));
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
pResponse.on('error', tmpComplete);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
tmpReq.on('error', tmpComplete);
|
|
561
|
-
|
|
562
|
-
// Disable socket timeout for long-running streaming dispatch
|
|
563
|
-
tmpReq.setTimeout(0);
|
|
564
|
-
|
|
565
|
-
tmpReq.write(JSON.stringify(pWorkItem));
|
|
566
|
-
tmpReq.end();
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// ================================================================
|
|
570
|
-
// HTTP Transport
|
|
571
|
-
// ================================================================
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Make an HTTP request to the Ultravisor server.
|
|
575
|
-
*
|
|
576
|
-
* @param {string} pMethod - HTTP method
|
|
577
|
-
* @param {string} pPath - URL path
|
|
578
|
-
* @param {object|null} pBody - Request body (JSON)
|
|
579
|
-
* @param {function} fCallback - function(pError, pResult)
|
|
580
|
-
*/
|
|
581
|
-
_httpRequest(pMethod, pPath, pBody, fCallback)
|
|
582
|
-
{
|
|
583
|
-
let tmpParsedURL;
|
|
584
|
-
try
|
|
585
|
-
{
|
|
586
|
-
tmpParsedURL = new URL(this._UltravisorURL);
|
|
587
|
-
}
|
|
588
|
-
catch (pError)
|
|
589
|
-
{
|
|
590
|
-
return fCallback(new Error('Invalid UltravisorURL: ' + this._UltravisorURL));
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
let tmpLib = tmpParsedURL.protocol === 'https:' ? libHTTPS : libHTTP;
|
|
594
|
-
|
|
595
|
-
let tmpHeaders = {
|
|
596
|
-
'Content-Type': 'application/json',
|
|
597
|
-
'Connection': 'keep-alive'
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
// Attach session cookie if available
|
|
601
|
-
if (this._SessionCookie)
|
|
602
|
-
{
|
|
603
|
-
tmpHeaders['Cookie'] = this._SessionCookie;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
let tmpOptions = {
|
|
607
|
-
hostname: tmpParsedURL.hostname,
|
|
608
|
-
port: tmpParsedURL.port || (tmpParsedURL.protocol === 'https:' ? 443 : 80),
|
|
609
|
-
path: pPath,
|
|
610
|
-
method: pMethod,
|
|
611
|
-
headers: tmpHeaders
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
let tmpCallbackFired = false;
|
|
615
|
-
|
|
616
|
-
let tmpComplete = (pError, pResult) =>
|
|
617
|
-
{
|
|
618
|
-
if (tmpCallbackFired) return;
|
|
619
|
-
tmpCallbackFired = true;
|
|
620
|
-
if (pError) return fCallback(pError);
|
|
621
|
-
return fCallback(null, pResult);
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
let tmpReq = tmpLib.request(tmpOptions, (pResponse) =>
|
|
625
|
-
{
|
|
626
|
-
let tmpData = '';
|
|
627
|
-
pResponse.on('data', (pChunk) => { tmpData += pChunk; });
|
|
628
|
-
pResponse.on('end', () =>
|
|
629
|
-
{
|
|
630
|
-
try
|
|
631
|
-
{
|
|
632
|
-
let tmpParsed = JSON.parse(tmpData);
|
|
633
|
-
if (pResponse.statusCode >= 400)
|
|
634
|
-
{
|
|
635
|
-
return tmpComplete(new Error(tmpParsed.Error || `HTTP ${pResponse.statusCode}`));
|
|
636
|
-
}
|
|
637
|
-
return tmpComplete(null, tmpParsed);
|
|
638
|
-
}
|
|
639
|
-
catch (pParseError)
|
|
640
|
-
{
|
|
641
|
-
return tmpComplete(new Error(`Invalid JSON response: ${tmpData.substring(0, 200)}`));
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
pResponse.on('error', tmpComplete);
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
tmpReq.on('error', (pError) =>
|
|
648
|
-
{
|
|
649
|
-
tmpComplete(pError);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
// Disable socket timeout for long-running dispatch requests
|
|
653
|
-
tmpReq.setTimeout(0);
|
|
654
|
-
|
|
655
|
-
if (pBody && (pMethod === 'POST' || pMethod === 'PUT'))
|
|
656
|
-
{
|
|
657
|
-
tmpReq.write(JSON.stringify(pBody));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
tmpReq.end();
|
|
234
|
+
this._Client.dispatchStream(pWorkItem, pCallbacks, fCallback);
|
|
661
235
|
}
|
|
662
236
|
}
|
|
663
237
|
|