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.
Files changed (79) hide show
  1. package/css/retold-remote.css +343 -20
  2. package/docs/.nojekyll +0 -0
  3. package/docs/README.md +64 -12
  4. package/docs/_cover.md +6 -6
  5. package/docs/_sidebar.md +2 -0
  6. package/docs/_topbar.md +1 -1
  7. package/docs/_version.json +7 -0
  8. package/docs/collections.md +30 -0
  9. package/docs/css/docuserve.css +327 -0
  10. package/docs/ebook-reader.md +75 -1
  11. package/docs/image-explorer.md +62 -2
  12. package/docs/index.html +39 -0
  13. package/docs/retold-catalog.json +254 -0
  14. package/docs/retold-keyword-index.json +31216 -0
  15. package/docs/server-setup.md +122 -91
  16. package/docs/stack-launcher.md +218 -0
  17. package/docs/synology.md +585 -0
  18. package/docs/ultravisor-configuration.md +5 -5
  19. package/docs/ultravisor-integration.md +4 -2
  20. package/package.json +20 -14
  21. package/source/Pict-Application-RetoldRemote.js +22 -0
  22. package/source/RetoldRemote-ExtensionMaps.js +1 -1
  23. package/source/cli/RetoldRemote-Server-Setup.js +460 -7
  24. package/source/cli/RetoldRemote-Stack-Launcher.js +563 -0
  25. package/source/cli/RetoldRemote-Stack-Run.js +41 -0
  26. package/source/cli/commands/RetoldRemote-Command-Serve.js +129 -54
  27. package/source/providers/CollectionManager-AddItems.js +166 -0
  28. package/source/providers/Pict-Provider-GalleryNavigation.js +55 -0
  29. package/source/providers/Pict-Provider-OperationStatus.js +597 -0
  30. package/source/providers/keyboard-handlers/KeyHandler-ImageExplorer.js +20 -1
  31. package/source/providers/keyboard-handlers/KeyHandler-Viewer.js +23 -0
  32. package/source/server/RetoldRemote-AudioWaveformService.js +49 -3
  33. package/source/server/RetoldRemote-CollectionExportService.js +763 -0
  34. package/source/server/RetoldRemote-CollectionService.js +5 -0
  35. package/source/server/RetoldRemote-EbookService.js +218 -3
  36. package/source/server/RetoldRemote-ImageService.js +221 -46
  37. package/source/server/RetoldRemote-MediaService.js +63 -4
  38. package/source/server/RetoldRemote-MetadataCache.js +25 -5
  39. package/source/server/RetoldRemote-OperationBroadcaster.js +363 -0
  40. package/source/server/RetoldRemote-SubimageService.js +680 -0
  41. package/source/server/RetoldRemote-ToolDetector.js +50 -0
  42. package/source/server/RetoldRemote-UltravisorBeacon.js +18 -3
  43. package/source/server/RetoldRemote-UltravisorDispatcher.js +65 -491
  44. package/source/server/RetoldRemote-UltravisorOperations.js +133 -20
  45. package/source/server/RetoldRemote-VideoFrameService.js +302 -9
  46. package/source/views/MediaViewer-EbookViewer.js +419 -1
  47. package/source/views/MediaViewer-PdfViewer.js +1050 -0
  48. package/source/views/PictView-Remote-AudioExplorer.js +77 -1
  49. package/source/views/PictView-Remote-CollectionsPanel.js +213 -0
  50. package/source/views/PictView-Remote-Gallery.js +365 -64
  51. package/source/views/PictView-Remote-ImageExplorer.js +1529 -44
  52. package/source/views/PictView-Remote-ImageViewer.js +2 -2
  53. package/source/views/PictView-Remote-Layout.js +58 -0
  54. package/source/views/PictView-Remote-MediaViewer.js +100 -25
  55. package/source/views/PictView-Remote-RegionsBrowser.js +554 -0
  56. package/source/views/PictView-Remote-SubimagesPanel.js +353 -0
  57. package/source/views/PictView-Remote-TopBar.js +1 -0
  58. package/source/views/PictView-Remote-VideoExplorer.js +77 -1
  59. package/web-application/css/docuserve.css +277 -23
  60. package/web-application/css/retold-remote.css +343 -20
  61. package/web-application/docs/README.md +64 -12
  62. package/web-application/docs/_cover.md +6 -6
  63. package/web-application/docs/_sidebar.md +2 -0
  64. package/web-application/docs/_topbar.md +1 -1
  65. package/web-application/docs/collections.md +30 -0
  66. package/web-application/docs/ebook-reader.md +75 -1
  67. package/web-application/docs/image-explorer.md +62 -2
  68. package/web-application/docs/server-setup.md +122 -91
  69. package/web-application/docs/stack-launcher.md +218 -0
  70. package/web-application/docs/synology.md +585 -0
  71. package/web-application/docs/ultravisor-configuration.md +5 -5
  72. package/web-application/docs/ultravisor-integration.md +4 -2
  73. package/web-application/js/pict-docuserve.min.js +12 -12
  74. package/web-application/js/pict.min.js +2 -2
  75. package/web-application/js/pict.min.js.map +1 -1
  76. package/web-application/retold-remote.js +6596 -1784
  77. package/web-application/retold-remote.js.map +1 -1
  78. package/web-application/retold-remote.min.js +75 -23
  79. 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 libHTTP = require('http');
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
- // Session cookie for authenticated requests
42
- this._SessionCookie = null;
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._authenticate((pAuthError) =>
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._httpRequest('GET', '/Beacon/Capabilities', null,
180
- (pError, pResult) =>
126
+ this._Client.getStatus((pError, pResult) =>
127
+ {
128
+ if (pError)
181
129
  {
182
- if (pError)
130
+ if (tmpSelf._Available)
183
131
  {
184
- if (tmpSelf._Available)
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
- let tmpPrevBeaconCount = tmpSelf._BeaconCount;
196
- let tmpPrevAvailable = tmpSelf._Available;
141
+ let tmpPrevBeaconCount = tmpSelf._BeaconCount;
142
+ let tmpPrevAvailable = tmpSelf._Available;
197
143
 
198
- tmpSelf._Capabilities = pResult.Capabilities || [];
199
- tmpSelf._BeaconCount = pResult.BeaconCount || 0;
200
- tmpSelf._Available = true;
144
+ tmpSelf._Capabilities = pResult.Capabilities || [];
145
+ tmpSelf._BeaconCount = pResult.BeaconCount || 0;
146
+ tmpSelf._Available = true;
201
147
 
202
- // Log state transitions
203
- if (!tmpPrevAvailable && tmpSelf._BeaconCount > 0)
204
- {
205
- tmpSelf.fable.log.info(`Ultravisor Dispatcher: connected — ${tmpSelf._BeaconCount} beacon(s), capabilities: [${tmpSelf._Capabilities.join(', ')}]`);
206
- }
207
- else if (tmpPrevBeaconCount === 0 && tmpSelf._BeaconCount > 0)
208
- {
209
- tmpSelf.fable.log.info(`Ultravisor Dispatcher: ${tmpSelf._BeaconCount} beacon(s) now available, capabilities: [${tmpSelf._Capabilities.join(', ')}]`);
210
- }
211
- else if (tmpSelf._BeaconCount === 0 && !tmpPrevAvailable)
212
- {
213
- tmpSelf.fable.log.warn('Ultravisor Dispatcher: connected but no beacons registered. Processing will be local until beacons connect.');
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
- if (fCallback) return fCallback(null);
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
- let tmpBody = JSON.stringify({
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
- tmpParsedURL = new URL(this._UltravisorURL);
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
- // Binary response — collect chunks as Buffers
299
- let tmpChunks = [];
300
- pResponse.on('data', (pChunk) => { tmpChunks.push(pChunk); });
301
- pResponse.on('end', () =>
302
- {
303
- let tmpBuffer = Buffer.concat(tmpChunks);
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
- // JSON response (error or no binary result)
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 (binary-frames-v1):
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 (same as dispatch())
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
- let tmpParsedURL;
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