retold-remote 0.0.15 → 0.0.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-remote",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Retold Remote - NAS media browser with gallery views and keyboard navigation",
5
5
  "main": "source/Pict-RetoldRemote-Bundle.js",
6
6
  "bin": {
@@ -34,22 +34,22 @@
34
34
  "dcraw": "^1.0.3",
35
35
  "epubjs": "^0.3.93",
36
36
  "exifr": "^7.1.3",
37
- "fable": "^3.1.63",
37
+ "fable": "^3.1.67",
38
38
  "fable-serviceproviderbase": "^3.0.19",
39
39
  "node-unrar-js": "^2.0.2",
40
40
  "orator": "^6.0.4",
41
41
  "orator-serviceserver-restify": "^2.0.9",
42
42
  "parime": "^1.0.3",
43
43
  "pdf-parse": "^1.1.1",
44
- "pict": "^1.0.357",
44
+ "pict": "^1.0.359",
45
45
  "pict-application": "^1.0.33",
46
46
  "pict-docuserve": "^0.0.32",
47
47
  "pict-provider": "^1.0.12",
48
- "pict-section-code": "^1.0.3",
48
+ "pict-section-code": "^1.0.4",
49
49
  "pict-section-filebrowser": "^0.0.2",
50
50
  "pict-service-commandlineutility": "^1.0.19",
51
51
  "pict-view": "^1.0.67",
52
- "retold-content-system": "^1.0.8",
52
+ "retold-content-system": "^1.0.12",
53
53
  "yauzl": "^3.2.0"
54
54
  },
55
55
  "optionalDependencies": {
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "puppeteer": "^24.0.0",
61
- "quackage": "^1.0.63"
61
+ "quackage": "^1.0.65"
62
62
  },
63
63
  "copyFilesSettings": {
64
64
  "whenFileExists": "overwrite"
@@ -752,7 +752,11 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
752
752
 
753
753
  if (!this._sharp && !this._capabilities.imagemagick)
754
754
  {
755
- return fCallback(new Error('Neither sharp nor ImageMagick is available.'));
755
+ // Neither Sharp nor ImageMagick available locally — check for Ultravisor
756
+ if (!(this._dispatcher && this._dispatcher.isAvailable()))
757
+ {
758
+ return fCallback(new Error('Neither sharp nor ImageMagick is available.'));
759
+ }
756
760
  }
757
761
 
758
762
  let tmpMaxDim = pMaxDimension || this.options.DefaultMaxPreviewDimension;
@@ -826,11 +830,20 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
826
830
  {
827
831
  this._doGeneratePreview(pAbsPath, pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpCacheDir, tmpManifestPath, tmpOutputPath, tmpStat, false, fCallback);
828
832
  }
829
- else
833
+ else if (this._capabilities.imagemagick)
830
834
  {
831
835
  // No Sharp available — use ImageMagick for standard images too
832
836
  this._doGeneratePreviewWithImageMagick(pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpManifestPath, tmpOutputPath, tmpStat, false, fCallback);
833
837
  }
838
+ else if (this._dispatcher && this._dispatcher.isAvailable())
839
+ {
840
+ // No local tools available — dispatch to Ultravisor beacon
841
+ this._doGeneratePreviewWithDispatcher(pAbsPath, pRelPath, tmpMaxDim, tmpCacheKey, tmpOutputFilename, tmpCacheDir, tmpManifestPath, tmpOutputPath, tmpStat, tmpIsRaw, fCallback);
842
+ }
843
+ else
844
+ {
845
+ return fCallback(new Error('No preview tools available.'));
846
+ }
834
847
  }
835
848
 
836
849
  /**
@@ -1087,6 +1100,101 @@ class RetoldRemoteImageService extends libFableServiceProviderBase
1087
1100
  }
1088
1101
  }
1089
1102
 
1103
+ /**
1104
+ * Generate a preview by dispatching to an Ultravisor beacon.
1105
+ * Uses the MediaConversion capability (ImageResize) with a shell
1106
+ * convert fallback. The result is written to the cache directory.
1107
+ *
1108
+ * @param {string} pInputPath - Absolute path to the source image
1109
+ * @param {string} pRelPath - Relative path (for response/logging)
1110
+ * @param {number} pMaxDim - Max dimension
1111
+ * @param {string} pCacheKey - Cache key
1112
+ * @param {string} pOutputFilename - Output filename
1113
+ * @param {string} pCacheDir - Cache directory path
1114
+ * @param {string} pManifestPath - Manifest file path
1115
+ * @param {string} pOutputPath - Output file path
1116
+ * @param {object} pStat - Original file stat
1117
+ * @param {boolean} pIsRaw - Whether this is a raw camera format
1118
+ * @param {Function} fCallback - Callback(pError, pResult)
1119
+ */
1120
+ _doGeneratePreviewWithDispatcher(pInputPath, pRelPath, pMaxDim, pCacheKey, pOutputFilename, pCacheDir, pManifestPath, pOutputPath, pStat, pIsRaw, fCallback)
1121
+ {
1122
+ let tmpSelf = this;
1123
+ let tmpRelPath;
1124
+
1125
+ try
1126
+ {
1127
+ tmpRelPath = libPath.relative(this.contentPath, pInputPath);
1128
+ }
1129
+ catch (pErr)
1130
+ {
1131
+ return fCallback(new Error('Could not resolve relative path for dispatch.'));
1132
+ }
1133
+
1134
+ if (!tmpRelPath || tmpRelPath.startsWith('..'))
1135
+ {
1136
+ return fCallback(new Error('File is outside content root.'));
1137
+ }
1138
+
1139
+ this._dispatcher.dispatchConversion(
1140
+ {
1141
+ Action: 'ImageResize',
1142
+ InputPath: tmpRelPath,
1143
+ OutputFilename: pOutputFilename,
1144
+ Width: pMaxDim,
1145
+ Height: pMaxDim,
1146
+ Format: 'jpeg',
1147
+ Quality: this.options.PreviewQuality || 85,
1148
+ AffinityKey: tmpRelPath,
1149
+ TimeoutMs: 120000,
1150
+ FallbackCommand: `convert "{SourcePath}"[0] -auto-orient -resize ${pMaxDim}x${pMaxDim} -quality ${this.options.PreviewQuality || 85} "{OutputPath}"`
1151
+ },
1152
+ (pDispatchError, pResult) =>
1153
+ {
1154
+ if (pDispatchError || !pResult || !pResult.OutputBuffer)
1155
+ {
1156
+ return fCallback(new Error('Ultravisor preview generation failed: ' + (pDispatchError ? pDispatchError.message : 'no output')));
1157
+ }
1158
+
1159
+ try
1160
+ {
1161
+ libFs.writeFileSync(pOutputPath, pResult.OutputBuffer);
1162
+ }
1163
+ catch (pWriteError)
1164
+ {
1165
+ return fCallback(new Error('Failed to write preview output: ' + pWriteError.message));
1166
+ }
1167
+
1168
+ let tmpResult =
1169
+ {
1170
+ Success: true,
1171
+ SourcePath: pRelPath,
1172
+ CacheKey: pCacheKey,
1173
+ OutputFilename: pOutputFilename,
1174
+ Width: pMaxDim,
1175
+ Height: pMaxDim,
1176
+ OrigWidth: 0,
1177
+ OrigHeight: 0,
1178
+ FileSize: pResult.OutputBuffer.length,
1179
+ NeedsPreview: true,
1180
+ IsRawFormat: pIsRaw,
1181
+ GeneratedAt: new Date().toISOString()
1182
+ };
1183
+
1184
+ try
1185
+ {
1186
+ libFs.writeFileSync(pManifestPath, JSON.stringify(tmpResult, null, '\t'));
1187
+ }
1188
+ catch (pWriteError)
1189
+ {
1190
+ tmpSelf.fable.log.warn(`Could not write preview manifest: ${pWriteError.message}`);
1191
+ }
1192
+
1193
+ tmpSelf.fable.log.info(`Generated image preview (Ultravisor): ${pRelPath}`);
1194
+ return fCallback(null, tmpResult);
1195
+ });
1196
+ }
1197
+
1090
1198
  // ---------------------------------------------------------------
1091
1199
  // DZI tile generation
1092
1200
  // ---------------------------------------------------------------
@@ -496,7 +496,9 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
496
496
  }
497
497
  }
498
498
 
499
- // Try Ultravisor dispatch as last resort for image thumbnails
499
+ // Try Ultravisor dispatch as last resort for image thumbnails.
500
+ // Uses structured MediaConversion capability (orator-conversion beacon)
501
+ // with shell convert fallback if MediaConversion is not available.
500
502
  if (this._dispatcher && this._dispatcher.isAvailable())
501
503
  {
502
504
  let tmpRelPath;
@@ -513,15 +515,19 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
513
515
  {
514
516
  let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
515
517
  let tmpOutputFilename = `thumbnail.${pFormat === 'webp' ? 'webp' : 'jpg'}`;
516
- let tmpCommand = `convert "{SourcePath}" -thumbnail ${pWidth}x${pHeight} -auto-orient ${tmpOutputFormat}:"{OutputPath}"`;
517
518
 
518
- this._dispatcher.dispatchMediaCommand(
519
+ this._dispatcher.dispatchConversion(
519
520
  {
520
- Command: tmpCommand,
521
+ Action: 'ImageResize',
521
522
  InputPath: tmpRelPath,
522
523
  OutputFilename: tmpOutputFilename,
524
+ Width: pWidth,
525
+ Height: pHeight,
526
+ Format: tmpOutputFormat,
527
+ Quality: 80,
523
528
  AffinityKey: tmpRelPath,
524
- TimeoutMs: 30000
529
+ TimeoutMs: 30000,
530
+ FallbackCommand: `convert "{SourcePath}" -thumbnail ${pWidth}x${pHeight} -auto-orient ${tmpOutputFormat}:"{OutputPath}"`
525
531
  },
526
532
  (pDispatchError, pResult) =>
527
533
  {
@@ -677,10 +683,11 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
677
683
  }
678
684
 
679
685
  /**
680
- * Fallback raw thumbnail generation: ImageMagick → exifr embedded preview.
686
+ * Fallback raw thumbnail generation: ImageMagick → Ultravisor MediaConversion → exifr embedded preview.
681
687
  */
682
688
  _generateRawThumbnailFallback(pFullPath, pWidth, pHeight, pFormat, fCallback)
683
689
  {
690
+ let tmpSelf = this;
684
691
  let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
685
692
 
686
693
  // Strategy 2: ImageMagick (may have dcraw delegate)
@@ -694,11 +701,67 @@ class RetoldRemoteMediaService extends libFableServiceProviderBase
694
701
  }
695
702
  catch (pError)
696
703
  {
697
- // Fall through to exifr
704
+ // Fall through to Ultravisor
705
+ }
706
+ }
707
+
708
+ // Strategy 2.5: Try Ultravisor MediaConversion (beacon may have Sharp
709
+ // even if local machine does not have ImageMagick)
710
+ if (this._dispatcher && this._dispatcher.isAvailable())
711
+ {
712
+ let tmpRelPath;
713
+ try
714
+ {
715
+ tmpRelPath = libPath.relative(this.contentPath, pFullPath);
716
+ }
717
+ catch (pErr)
718
+ {
719
+ tmpRelPath = null;
720
+ }
721
+
722
+ if (tmpRelPath && !tmpRelPath.startsWith('..'))
723
+ {
724
+ let tmpOutputFilename = `thumbnail.${pFormat === 'webp' ? 'webp' : 'jpg'}`;
725
+
726
+ this._dispatcher.dispatchConversion(
727
+ {
728
+ Action: 'ImageResize',
729
+ InputPath: tmpRelPath,
730
+ OutputFilename: tmpOutputFilename,
731
+ Width: pWidth,
732
+ Height: pHeight,
733
+ Format: tmpOutputFormat,
734
+ Quality: 80,
735
+ AffinityKey: tmpRelPath,
736
+ TimeoutMs: 60000,
737
+ FallbackCommand: `convert "{SourcePath}" -thumbnail ${pWidth}x${pHeight} -auto-orient ${tmpOutputFormat}:"{OutputPath}"`
738
+ },
739
+ (pDispatchError, pResult) =>
740
+ {
741
+ if (!pDispatchError && pResult && pResult.OutputBuffer)
742
+ {
743
+ tmpSelf.fable.log.info(`Raw thumbnail generated via Ultravisor for ${tmpRelPath}`);
744
+ return fCallback(null, pResult.OutputBuffer);
745
+ }
746
+
747
+ // Fall through to exifr embedded preview
748
+ tmpSelf._generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback);
749
+ });
750
+ return;
698
751
  }
699
752
  }
700
753
 
701
- // Strategy 3: Extract embedded JPEG preview via exifr, resize with sharp
754
+ return this._generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback);
755
+ }
756
+
757
+ /**
758
+ * Strategy 3 for raw thumbnails: extract embedded JPEG preview via exifr,
759
+ * resize with sharp. Most cameras embed a full-size JPEG preview in the raw file.
760
+ */
761
+ _generateRawThumbnailExifr(pFullPath, pWidth, pHeight, pFormat, fCallback)
762
+ {
763
+ let tmpOutputFormat = pFormat === 'webp' ? 'webp' : 'jpeg';
764
+
702
765
  if (this.capabilities.sharp)
703
766
  {
704
767
  let tmpSharp = this.capabilities.sharpModule;
@@ -198,6 +198,317 @@ class RetoldRemoteUltravisorDispatcher extends libFableServiceProviderBase
198
198
  });
199
199
  }
200
200
 
201
+ /**
202
+ * Check if a specific capability is available on any connected beacon.
203
+ *
204
+ * @param {string} pCapability - The capability name (e.g. 'MediaConversion', 'Shell')
205
+ * @returns {boolean} True if at least one beacon has this capability
206
+ */
207
+ hasCapability(pCapability)
208
+ {
209
+ return this._Available && this._Capabilities.indexOf(pCapability) >= 0;
210
+ }
211
+
212
+ /**
213
+ * Dispatch a structured media conversion to Ultravisor using the
214
+ * MediaConversion capability (orator-conversion beacon provider).
215
+ *
216
+ * Falls back to dispatchMediaCommand() (Shell) if MediaConversion
217
+ * is not available.
218
+ *
219
+ * @param {object} pOptions - Conversion options
220
+ * @param {string} pOptions.Action - Conversion action (e.g. 'ImageResize', 'PdfPageToPng')
221
+ * @param {string} [pOptions.InputPath] - Relative path to source file (from content root)
222
+ * @param {string} [pOptions.InputFilename] - Filename for the downloaded source
223
+ * @param {string} [pOptions.OutputFilename] - Name of the output file
224
+ * @param {number} [pOptions.Width] - Width for ImageResize
225
+ * @param {number} [pOptions.Height] - Height for ImageResize
226
+ * @param {string} [pOptions.Format] - Output format for ImageResize
227
+ * @param {number} [pOptions.Quality] - Quality for lossy formats
228
+ * @param {number} [pOptions.Page] - PDF page number (1-based)
229
+ * @param {number} [pOptions.LongSidePixels] - Max dimension for sized PDF renders
230
+ * @param {string} [pOptions.AffinityKey] - Affinity routing key
231
+ * @param {number} [pOptions.TimeoutMs] - Timeout in ms (default 300000)
232
+ * @param {function} fCallback - function(pError, pResult)
233
+ */
234
+ dispatchConversion(pOptions, fCallback)
235
+ {
236
+ if (!this.hasCapability('MediaConversion'))
237
+ {
238
+ // Fall back to shell dispatch if caller provides a Command
239
+ if (pOptions.FallbackCommand)
240
+ {
241
+ return this.dispatchMediaCommand({
242
+ Command: pOptions.FallbackCommand,
243
+ InputPath: pOptions.InputPath,
244
+ InputFilename: pOptions.InputFilename,
245
+ OutputFilename: pOptions.OutputFilename,
246
+ AffinityKey: pOptions.AffinityKey,
247
+ TimeoutMs: pOptions.TimeoutMs
248
+ }, fCallback);
249
+ }
250
+ return fCallback(new Error('MediaConversion capability not available and no fallback command provided'));
251
+ }
252
+
253
+ let tmpSettings = {
254
+ InputFile: pOptions.InputFilename ||
255
+ (pOptions.InputPath ? pOptions.InputPath.split('/').pop() : 'source_file'),
256
+ OutputFile: pOptions.OutputFilename || 'output'
257
+ };
258
+
259
+ // Pass through action-specific settings
260
+ if (pOptions.Width) tmpSettings.Width = pOptions.Width;
261
+ if (pOptions.Height) tmpSettings.Height = pOptions.Height;
262
+ if (pOptions.Format) tmpSettings.Format = pOptions.Format;
263
+ if (pOptions.Quality) tmpSettings.Quality = pOptions.Quality;
264
+ if (pOptions.Page) tmpSettings.Page = pOptions.Page;
265
+ if (pOptions.LongSidePixels) tmpSettings.LongSidePixels = pOptions.LongSidePixels;
266
+
267
+ // Set up source file download
268
+ if (pOptions.InputPath && this._ContentAPIURL)
269
+ {
270
+ let tmpEncodedPath = pOptions.InputPath.split('/').map(encodeURIComponent).join('/');
271
+ tmpSettings.SourceURL = this._ContentAPIURL + '/content/' + tmpEncodedPath;
272
+ tmpSettings.SourceFilename = tmpSettings.InputFile;
273
+ }
274
+
275
+ // Set up output file collection
276
+ if (pOptions.OutputFilename)
277
+ {
278
+ tmpSettings.ReturnOutputAsBase64 = true;
279
+ }
280
+
281
+ let tmpWorkItem = {
282
+ Capability: 'MediaConversion',
283
+ Action: pOptions.Action || 'ImageResize',
284
+ Settings: tmpSettings,
285
+ AffinityKey: pOptions.AffinityKey || '',
286
+ TimeoutMs: pOptions.TimeoutMs || 300000
287
+ };
288
+
289
+ this.dispatch(tmpWorkItem,
290
+ (pError, pResult) =>
291
+ {
292
+ if (pError)
293
+ {
294
+ return fCallback(pError);
295
+ }
296
+
297
+ // If we have base64 output data, decode it to a Buffer
298
+ if (pResult.Outputs && pResult.Outputs.OutputData)
299
+ {
300
+ try
301
+ {
302
+ pResult.OutputBuffer = Buffer.from(pResult.Outputs.OutputData, 'base64');
303
+ }
304
+ catch (pDecodeError)
305
+ {
306
+ return fCallback(new Error('Failed to decode output data: ' + pDecodeError.message));
307
+ }
308
+ }
309
+
310
+ return fCallback(null, pResult);
311
+ });
312
+ }
313
+
314
+ // ================================================================
315
+ // Streaming Dispatch (binary-framed)
316
+ // ================================================================
317
+
318
+ /**
319
+ * Dispatch a work item with binary-framed streaming.
320
+ *
321
+ * Uses the /Beacon/Work/DispatchStream endpoint which returns a
322
+ * binary frame stream instead of a single JSON response. This
323
+ * enables real-time progress updates and efficient binary output
324
+ * transfer without base64 re-encoding overhead.
325
+ *
326
+ * Frame protocol (binary-frames-v1):
327
+ * [1 byte type][4 bytes payload length (uint32 big-endian)][payload]
328
+ * Type 0x01: Progress (JSON: { Percent, Message, Step, TotalSteps })
329
+ * Type 0x02: Intermediate (raw binary: e.g. thumbnail preview)
330
+ * Type 0x03: Final output (raw binary: completed file)
331
+ * Type 0x04: Result (JSON: { Success, Outputs, Log })
332
+ * Type 0x05: Error (JSON: { Error })
333
+ *
334
+ * @param {object} pWorkItem - Work item details (same as dispatch())
335
+ * @param {object} pCallbacks - Event callbacks:
336
+ * {
337
+ * onProgress: function({ Percent, Message, Step, TotalSteps }) — optional
338
+ * onBinaryData: function(Buffer) — optional, intermediate binary data
339
+ * onError: function({ Error }) — optional, non-fatal error notification
340
+ * }
341
+ * @param {function} fCallback - function(pError, pResult) called on completion.
342
+ * pResult includes OutputBuffer (Buffer) if final binary output was streamed.
343
+ */
344
+ dispatchStream(pWorkItem, pCallbacks, fCallback)
345
+ {
346
+ if (!this._UltravisorURL)
347
+ {
348
+ return fCallback(new Error('Ultravisor Dispatcher: not configured'));
349
+ }
350
+
351
+ let tmpParsedURL;
352
+ try
353
+ {
354
+ tmpParsedURL = new URL(this._UltravisorURL);
355
+ }
356
+ catch (pError)
357
+ {
358
+ return fCallback(new Error('Invalid UltravisorURL: ' + this._UltravisorURL));
359
+ }
360
+
361
+ let tmpLib = tmpParsedURL.protocol === 'https:' ? libHTTPS : libHTTP;
362
+
363
+ let tmpOptions = {
364
+ hostname: tmpParsedURL.hostname,
365
+ port: tmpParsedURL.port || (tmpParsedURL.protocol === 'https:' ? 443 : 80),
366
+ path: '/Beacon/Work/DispatchStream',
367
+ method: 'POST',
368
+ headers: {
369
+ 'Content-Type': 'application/json',
370
+ 'Connection': 'keep-alive'
371
+ }
372
+ };
373
+
374
+ let tmpCallbackFired = false;
375
+ let tmpComplete = (pError, pResult) =>
376
+ {
377
+ if (tmpCallbackFired) { return; }
378
+ tmpCallbackFired = true;
379
+ fCallback(pError, pResult);
380
+ };
381
+
382
+ let tmpReq = tmpLib.request(tmpOptions, (pResponse) =>
383
+ {
384
+ // Non-streaming error response (4xx/5xx before stream starts)
385
+ if (pResponse.statusCode >= 400)
386
+ {
387
+ let tmpData = '';
388
+ pResponse.on('data', (pChunk) => { tmpData += pChunk; });
389
+ pResponse.on('end', () =>
390
+ {
391
+ try
392
+ {
393
+ let tmpParsed = JSON.parse(tmpData);
394
+ tmpComplete(new Error(tmpParsed.Error || `HTTP ${pResponse.statusCode}`));
395
+ }
396
+ catch (pParseError)
397
+ {
398
+ tmpComplete(new Error(`HTTP ${pResponse.statusCode}: ${tmpData.substring(0, 200)}`));
399
+ }
400
+ });
401
+ pResponse.on('error', tmpComplete);
402
+ return;
403
+ }
404
+
405
+ // Binary frame stream parser
406
+ let tmpBuffer = Buffer.alloc(0);
407
+ let tmpLastResult = null;
408
+ let tmpBinaryChunks = [];
409
+
410
+ pResponse.on('data', (pChunk) =>
411
+ {
412
+ tmpBuffer = Buffer.concat([tmpBuffer, pChunk]);
413
+
414
+ // Parse complete frames from the buffer
415
+ while (tmpBuffer.length >= 5)
416
+ {
417
+ let tmpPayloadLen = tmpBuffer.readUInt32BE(1);
418
+
419
+ if (tmpBuffer.length < 5 + tmpPayloadLen)
420
+ {
421
+ break; // Need more data for this frame
422
+ }
423
+
424
+ let tmpType = tmpBuffer.readUInt8(0);
425
+ let tmpPayload = tmpBuffer.slice(5, 5 + tmpPayloadLen);
426
+ tmpBuffer = tmpBuffer.slice(5 + tmpPayloadLen);
427
+
428
+ switch (tmpType)
429
+ {
430
+ case 0x01: // Progress
431
+ if (pCallbacks && pCallbacks.onProgress)
432
+ {
433
+ try
434
+ {
435
+ pCallbacks.onProgress(JSON.parse(tmpPayload.toString()));
436
+ }
437
+ catch (pParseError)
438
+ {
439
+ // Ignore malformed progress frames
440
+ }
441
+ }
442
+ break;
443
+
444
+ case 0x02: // Intermediate binary data
445
+ if (pCallbacks && pCallbacks.onBinaryData)
446
+ {
447
+ pCallbacks.onBinaryData(Buffer.from(tmpPayload));
448
+ }
449
+ break;
450
+
451
+ case 0x03: // Final binary output
452
+ tmpBinaryChunks.push(Buffer.from(tmpPayload));
453
+ break;
454
+
455
+ case 0x04: // Result metadata
456
+ try
457
+ {
458
+ tmpLastResult = JSON.parse(tmpPayload.toString());
459
+ }
460
+ catch (pParseError)
461
+ {
462
+ // Ignore malformed result frames
463
+ }
464
+ break;
465
+
466
+ case 0x05: // Error
467
+ if (pCallbacks && pCallbacks.onError)
468
+ {
469
+ try
470
+ {
471
+ pCallbacks.onError(JSON.parse(tmpPayload.toString()));
472
+ }
473
+ catch (pParseError)
474
+ {
475
+ // Ignore malformed error frames
476
+ }
477
+ }
478
+ break;
479
+ }
480
+ }
481
+ });
482
+
483
+ pResponse.on('end', () =>
484
+ {
485
+ if (tmpLastResult)
486
+ {
487
+ // Attach final binary output as a Buffer
488
+ if (tmpBinaryChunks.length > 0)
489
+ {
490
+ tmpLastResult.OutputBuffer = Buffer.concat(tmpBinaryChunks);
491
+ }
492
+ tmpComplete(null, tmpLastResult);
493
+ }
494
+ else
495
+ {
496
+ tmpComplete(new Error('Stream ended without result frame'));
497
+ }
498
+ });
499
+
500
+ pResponse.on('error', tmpComplete);
501
+ });
502
+
503
+ tmpReq.on('error', tmpComplete);
504
+
505
+ // Disable socket timeout for long-running streaming dispatch
506
+ tmpReq.setTimeout(0);
507
+
508
+ tmpReq.write(JSON.stringify(pWorkItem));
509
+ tmpReq.end();
510
+ }
511
+
201
512
  // ================================================================
202
513
  // HTTP Transport
203
514
  // ================================================================