@shapediver/viewer.session-engine.session-engine 2.11.0 → 2.12.0

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.
@@ -4,7 +4,7 @@ import {
4
4
  latestVersion,
5
5
  validate,
6
6
  versions
7
- } from '@shapediver/viewer.settings';
7
+ } from '@shapediver/viewer.settings';
8
8
  import {
9
9
  create,
10
10
  isGBResponseError,
@@ -21,7 +21,7 @@ import {
21
21
  ShapeDiverResponseOutput,
22
22
  ShapeDiverSdk,
23
23
  ShapeDiverSdkConfigType
24
- } from '@shapediver/sdk.geometry-api-sdk-v2';
24
+ } from '@shapediver/sdk.geometry-api-sdk-v2';
25
25
  import {
26
26
  EventEngine,
27
27
  EVENTTYPE,
@@ -36,7 +36,7 @@ import {
36
36
  StateEngine,
37
37
  SystemInfo,
38
38
  UuidGenerator
39
- } from '@shapediver/viewer.shared.services';
39
+ } from '@shapediver/viewer.shared.services';
40
40
  import { Export } from './dto/Export';
41
41
  import { FileParameter } from './dto/FileParameter';
42
42
  import { IExport } from '../interfaces/dto/IExport';
@@ -45,13 +45,13 @@ import { IOutput } from '../interfaces/dto/IOutput';
45
45
  import { IParameter } from '../interfaces/dto/IParameter';
46
46
  import { ISessionEngine, ISettingsSections, PARAMETER_TYPE } from '../interfaces/ISessionEngine';
47
47
  import { ISessionTreeNode } from '../interfaces/ISessionTreeNode';
48
- import { ITaskEvent, TASK_TYPE } from '@shapediver/viewer.shared.types';
48
+ import { IOutputEvent, ITaskEvent, TASK_TYPE } from '@shapediver/viewer.shared.types';
49
49
  import {
50
50
  ITree,
51
51
  ITreeNode,
52
52
  Tree,
53
53
  TreeNode
54
- } from '@shapediver/viewer.shared.node-tree';
54
+ } from '@shapediver/viewer.shared.node-tree';
55
55
  import { Output } from './dto/Output';
56
56
  import { OutputDelayException } from './OutputDelayException';
57
57
  import { OutputLoader, OutputLoaderTaskEventInfo } from './OutputLoader';
@@ -84,7 +84,7 @@ export class SessionEngine implements ISessionEngine {
84
84
  private readonly _ticket?: string;
85
85
  private readonly _uuidGenerator = UuidGenerator.instance;
86
86
 
87
- #customizationProcess!: string;
87
+ #customizationProcess?: string;
88
88
  #parameterHistory: {
89
89
  [key: string]: {
90
90
  value: unknown,
@@ -267,7 +267,7 @@ export class SessionEngine implements ISessionEngine {
267
267
 
268
268
  // #endregion Public Accessors (25)
269
269
 
270
- // #region Public Methods (25)
270
+ // #region Public Methods (27)
271
271
 
272
272
  public applySettings(response: ShapeDiverResponseDto, sections?: ISettingsSections) {
273
273
  sections = sections || {};
@@ -317,7 +317,7 @@ export class SessionEngine implements ISessionEngine {
317
317
  if (sections.session.parameter.hidden) this.parameters[p].hidden = settings.session[p].hidden || false;
318
318
  }
319
319
 
320
- if (response.parameters && response.parameters[p]) {
320
+ if (response.parameters && response.parameters[p] && !((this.parameters[p] instanceof FileParameter) || this.parameters[p].type.startsWith('s'))) {
321
321
  if (sections.session.parameter.value) this.parameters[p].value = response.parameters[p].defval !== undefined ? response.parameters[p].defval : this.parameters[p].value;
322
322
  }
323
323
  }
@@ -409,6 +409,15 @@ export class SessionEngine implements ISessionEngine {
409
409
  return this.#parameterHistoryForward.length > 0;
410
410
  }
411
411
 
412
+ public cancelCustomization() {
413
+ if(this.#customizationProcess) {
414
+ for (const r in this._stateEngine.renderingEngines)
415
+ if (this._stateEngine.renderingEngines[r].busy.includes(this.#customizationProcess))
416
+ this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(this.#customizationProcess), 1);
417
+ }
418
+ this.#customizationProcess = undefined;
419
+ }
420
+
412
421
  public async close(retry = false): Promise<void> {
413
422
  this.checkAvailability('close');
414
423
 
@@ -466,51 +475,14 @@ export class SessionEngine implements ISessionEngine {
466
475
  fileParameterIds[parameterId] = await (<IFileParameter>this.parameters[parameterId]).upload();
467
476
 
468
477
  // OPTION TO SKIP - PART 1a
469
- if (this.#customizationProcess !== customizationId) {
470
- for (const r in this._stateEngine.renderingEngines)
471
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
472
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
473
-
474
- this._logger.debug(`Session(${this.id}).customize: Session customization was exceeded by other customization request.`);
475
-
476
- const eventCancel1a: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session customization was exceeded by other customization request' };
477
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1a);
478
- return new SessionTreeNode();
479
- } else if (this._closed === true) {
480
- for (const r in this._stateEngine.renderingEngines)
481
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
482
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
483
-
484
- this._logger.debug(`Session(${this.id}).customize: Session was closed during customization request.`);
485
-
486
- const eventCancel1a: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session was closed during customization request' };
487
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1a);
488
- return new SessionTreeNode();
489
- }
478
+ const cancelResult = this.cancelProcess(customizationId, eventId, TASK_TYPE.SESSION_CUSTOMIZATION, 1, { sessionId: this.id });
479
+ if (cancelResult) return cancelResult;
490
480
  }
491
481
  }
492
482
 
493
483
  // OPTION TO SKIP - PART 1b
494
- if (this.#customizationProcess !== customizationId) {
495
- for (const r in this._stateEngine.renderingEngines)
496
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
497
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
498
-
499
- const eventCancel1b: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session customization was exceeded by other customization request' };
500
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1b);
501
- this._logger.debug(`Session(${this.id}).customize: Session customization was exceeded by other customization request.`);
502
- return new SessionTreeNode();
503
- } else if (this._closed === true) {
504
- for (const r in this._stateEngine.renderingEngines)
505
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
506
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
507
-
508
- this._logger.debug(`Session(${this.id}).customize: Session was closed during customization request.`);
509
-
510
- const eventCancel1b: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session was closed during customization request' };
511
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1b);
512
- return new SessionTreeNode();
513
- }
484
+ const cancelResult = this.cancelProcess(customizationId, eventId, TASK_TYPE.SESSION_CUSTOMIZATION, 1, { sessionId: this.id });
485
+ if (cancelResult) return cancelResult;
514
486
 
515
487
  // assign the uploaded parameters
516
488
  for (const parameterId in fileParameterIds)
@@ -539,6 +511,8 @@ export class SessionEngine implements ISessionEngine {
539
511
  const eventRequest: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 0.1, data: { sessionId: this.id }, status: 'Sending customization request' };
540
512
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_PROCESS, eventRequest);
541
513
 
514
+ const oldOutputVersions = this._outputLoader.getCurrentOutputVersions();
515
+
542
516
  const newNode = await this.customizeInternal(() => this.#customizationProcess !== customizationId, {
543
517
  eventId,
544
518
  type: TASK_TYPE.SESSION_CUSTOMIZATION,
@@ -549,29 +523,32 @@ export class SessionEngine implements ISessionEngine {
549
523
  data: { sessionId: this.id }
550
524
  });
551
525
 
526
+ // OPTION TO SKIP - PART 2
527
+ const cancelResult2 = this.cancelProcess(customizationId, eventId, TASK_TYPE.SESSION_CUSTOMIZATION, 1, { sessionId: this.id });
528
+ if (cancelResult2) return cancelResult2;
529
+
530
+ const newOutputVersions = this._outputLoader.getCurrentOutputVersions();
531
+
552
532
  const eventSceneUpdate: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 0.9, data: { sessionId: this.id }, status: 'Updating scene' };
553
533
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_PROCESS, eventSceneUpdate);
554
534
 
555
- // OPTION TO SKIP - PART 2
556
- if (this.#customizationProcess !== customizationId) {
557
- for (const r in this._stateEngine.renderingEngines)
558
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
559
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
560
-
561
- const eventCancel2: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session customization was exceeded by other customization request' };
562
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel2);
563
- this._logger.debug(`Session(${this.id}).customize: Session customization was exceeded by other customization request.`);
564
- return newNode;
565
- } else if ((this._closed as boolean) === true) { // I get a TS warning here that the type of _closed is "false", I think TS doesn't get that there is a promise inbetween
566
- for (const r in this._stateEngine.renderingEngines)
567
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
568
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
535
+ // call the update callbacks
536
+ if (waitForViewportUpdate === false) {
537
+ for (const outputId in this.outputs) {
538
+ if (oldOutputVersions[outputId] !== newOutputVersions[outputId]) {
539
+ this._eventEngine.emitEvent(EVENTTYPE.OUTPUT.OUTPUT_UPDATED, <IOutputEvent>{
540
+ outputId: outputId,
541
+ outputVersion: newOutputVersions[outputId],
542
+ newNode: newNode.children.find(c => c.name === outputId)!,
543
+ oldNode: oldNode.children.find(c => c.name === outputId)!
544
+ });
545
+ }
546
+ }
569
547
 
570
- this._logger.debug(`Session(${this.id}).customize: Session was closed during customization request.`);
548
+ await this.waitForUpdateCallbacks(newOutputVersions, oldOutputVersions, newNode, oldNode);
571
549
 
572
- const eventCancel2: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session was closed during customization request' };
573
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel2);
574
- return new SessionTreeNode();
550
+ const cancelResult = this.cancelProcess(customizationId, eventId, TASK_TYPE.SESSION_CUSTOMIZATION, 1, { sessionId: this.id });
551
+ if (cancelResult) return cancelResult;
575
552
  }
576
553
 
577
554
  // if this is not a call by the goBack or goForward functions, add the parameter values to the history and delete the forward history
@@ -617,20 +594,37 @@ export class SessionEngine implements ISessionEngine {
617
594
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_END, eventEnd);
618
595
 
619
596
  // update the viewports
620
- if (waitForViewportUpdate)
597
+ if (waitForViewportUpdate) {
621
598
  for (const r in this._stateEngine.renderingEngines)
622
599
  if (!this.excludeViewports.includes(this._stateEngine.renderingEngines[r].id))
623
600
  this._stateEngine.renderingEngines[r].update(`SessionEngine(${this.id}).customize`);
624
601
 
625
- // call the update callback function on the session
626
- if (this._updateCallback) this._updateCallback(newNode, oldNode);
602
+
603
+ for (const outputId in this.outputs) {
604
+ if (oldOutputVersions[outputId] !== newOutputVersions[outputId]) {
605
+ this._eventEngine.emitEvent(EVENTTYPE.OUTPUT.OUTPUT_UPDATED, <IOutputEvent>{
606
+ outputId: outputId,
607
+ outputVersion: newOutputVersions[outputId],
608
+ newNode: newNode.children.find(c => c.name === outputId)!,
609
+ oldNode: oldNode.children.find(c => c.name === outputId)!
610
+ });
611
+ }
612
+ }
627
613
 
628
- // call the update callback functions on the outputs
629
- for (const outputId in this.outputs)
630
- this.outputs[outputId].triggerUpdateCallback(
631
- newNode.children.find(c => c.name === outputId)!,
632
- oldNode.children.find(c => c.name === outputId)!
633
- );
614
+ // call the update callbacks
615
+ await this.waitForUpdateCallbacks(newOutputVersions, oldOutputVersions, newNode, oldNode);
616
+
617
+ const cancelResult = this.cancelProcess(customizationId, eventId, TASK_TYPE.SESSION_CUSTOMIZATION, 1, { sessionId: this.id });
618
+ if (cancelResult) return cancelResult;
619
+ }
620
+
621
+ if (!waitForViewportUpdate) {
622
+ setTimeout(() => {
623
+ for (const r in this._stateEngine.renderingEngines)
624
+ if (!this.excludeViewports.includes(this._stateEngine.renderingEngines[r].id))
625
+ this._stateEngine.renderingEngines[r].update(`SessionEngine(${this.id}).customize`);
626
+ }, 0);
627
+ }
634
628
 
635
629
  return this.node;
636
630
  } catch (e) {
@@ -645,7 +639,7 @@ export class SessionEngine implements ISessionEngine {
645
639
  }
646
640
  }
647
641
 
648
- public async customizeParallel(parameterValues: { [key: string]: string }): Promise<ITreeNode> {
642
+ public async customizeParallel(parameterValues: { [key: string]: string }, loadOutputs = true): Promise<ISessionTreeNode | ShapeDiverResponseDto> {
649
643
  const eventId = this._uuidGenerator.create();
650
644
 
651
645
  const eventStart: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 0, data: { sessionId: this.id }, status: 'Customizing session' };
@@ -659,7 +653,7 @@ export class SessionEngine implements ISessionEngine {
659
653
  for (const parameterId in this.parameters)
660
654
  parameterSet[parameterId] = parameterValues[parameterId] !== undefined ? (' ' + parameterValues[parameterId]).slice(1) : this.parameters[parameterId].stringify();
661
655
 
662
- const newNode = await this.customizeSession(parameterSet, () => false, {
656
+ const result = await this.customizeSession(parameterSet, () => false, {
663
657
  eventId,
664
658
  type: TASK_TYPE.SESSION_CUSTOMIZATION,
665
659
  progressRange: {
@@ -667,12 +661,14 @@ export class SessionEngine implements ISessionEngine {
667
661
  max: 1
668
662
  },
669
663
  data: { sessionId: this.id }
670
- }, true);
671
- newNode.excludeViewports = JSON.parse(JSON.stringify(this._excludeViewports));
664
+ }, true, loadOutputs);
665
+
666
+ if (result instanceof SessionTreeNode)
667
+ result.excludeViewports = JSON.parse(JSON.stringify(this._excludeViewports));
672
668
 
673
669
  const eventEnd: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session customized' };
674
670
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_END, eventEnd);
675
- return newNode;
671
+ return result;
676
672
  }
677
673
 
678
674
  public async goBack(): Promise<ITreeNode> {
@@ -772,6 +768,63 @@ export class SessionEngine implements ISessionEngine {
772
768
  }
773
769
  }
774
770
 
771
+ public async loadCachedOutputsParallel(outputMapping: { [key: string]: string }, taskEventInfo?: OutputLoaderTaskEventInfo, retry = false): Promise<{ [key: string]: ITreeNode | undefined }> {
772
+ this.checkAvailability();
773
+ // if there is already task event info, use it
774
+ // this happens after a retry
775
+ const eventId = taskEventInfo ? taskEventInfo.eventId : this._uuidGenerator.create();
776
+ const eventType = taskEventInfo ? taskEventInfo.type : TASK_TYPE.SESSION_OUTPUTS_LOADING;
777
+ const eventData = taskEventInfo ? taskEventInfo.data : { sessionId: this.id };
778
+
779
+ taskEventInfo = taskEventInfo ? taskEventInfo : {
780
+ eventId,
781
+ type: eventType,
782
+ progressRange: {
783
+ min: 0,
784
+ max: 1
785
+ },
786
+ data: eventData
787
+ };
788
+
789
+ try {
790
+ // send start event if this function was called initially
791
+ if (!taskEventInfo) {
792
+ const eventStart: ITaskEvent = { type: eventType, id: eventId, progress: 0, data: eventData, status: 'Loading cached outputs' };
793
+ this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_START, eventStart);
794
+ }
795
+
796
+ // get the cached outputs
797
+ const responseDto = await this._sdk.output.getCache(this._sessionId!, outputMapping);
798
+
799
+ // create atomic output api objects for them
800
+ const outputs: {
801
+ [key: string]: IOutput;
802
+ } = {};
803
+ for (const outputId in responseDto.outputs) {
804
+ responseDto.outputs[outputId].id = outputId;
805
+ outputs[outputId] = new Output(<ShapeDiverResponseOutput>responseDto.outputs[outputId], this);
806
+ }
807
+
808
+ // process the output data
809
+ const node = await this._outputLoader.loadOutputs(this._responseDto!.model?.name || 'model', outputs, {}, taskEventInfo, false);
810
+
811
+ // send the end event once done
812
+ const eventEnd: ITaskEvent = { type: eventType, id: eventId, progress: 1, data: eventData, status: 'Loaded cached outputs' };
813
+ this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_END, eventEnd);
814
+
815
+ // create a mapping with a dictionary for the id of the outputs
816
+ const outputNodeMapping: { [key: string]: ITreeNode | undefined } = {};
817
+ for (const outputId in outputMapping)
818
+ outputNodeMapping[outputId] = node.children.find(n => n.name === outputId);
819
+
820
+ return outputNodeMapping;
821
+ }
822
+ catch (e) {
823
+ await this.handleError(e, retry);
824
+ return await this.loadCachedOutputsParallel(outputMapping, taskEventInfo, true);
825
+ }
826
+ }
827
+
775
828
  /**
776
829
  * Load the outputs and return the scene graph node of the result.
777
830
  * In case the outputs have a delay property, another customization request with the parameter set is sent.
@@ -788,15 +841,8 @@ export class SessionEngine implements ISessionEngine {
788
841
  try {
789
842
  const node = await this._outputLoader.loadOutputs(this._responseDto!.model?.name || 'model', o, of, taskEventInfo);
790
843
  node.data.push(new SessionData(this._responseDto!));
791
-
792
844
  if (cancelRequest()) return node;
793
-
794
- if (this._automaticSceneUpdate) this.removeFromSceneTree(this._node);
795
- this._node = node;
796
- if (this._automaticSceneUpdate && this._closed === false) this.addToSceneTree(this._node);
797
-
798
- this.node.excludeViewports = JSON.parse(JSON.stringify(this._excludeViewports));
799
-
845
+ node.excludeViewports = JSON.parse(JSON.stringify(this._excludeViewports));
800
846
  return node;
801
847
  }
802
848
  catch (e) {
@@ -1139,6 +1185,8 @@ export class SessionEngine implements ISessionEngine {
1139
1185
  const eventRequest: ITaskEvent = { type: eventType, id: eventId, progress: taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 0.1 + taskEventInfo.progressRange.min : 0.1, data: eventData, status: 'Loading outputs' };
1140
1186
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_PROCESS, eventRequest);
1141
1187
 
1188
+ const oldOutputVersions = this._outputLoader.getCurrentOutputVersions();
1189
+
1142
1190
  const newNode = await this.loadOutputs(() => this.#customizationProcess !== customizationId, {
1143
1191
  eventId,
1144
1192
  type: eventType,
@@ -1149,29 +1197,33 @@ export class SessionEngine implements ISessionEngine {
1149
1197
  data: eventData
1150
1198
  });
1151
1199
 
1200
+ const newOutputVersions = this._outputLoader.getCurrentOutputVersions();
1201
+
1152
1202
  const eventSceneUpdate: ITaskEvent = { type: eventType, id: eventId, progress: taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 0.9 + taskEventInfo.progressRange.min : 0.9, data: eventData, status: 'Updating scene' };
1153
1203
  this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_PROCESS, eventSceneUpdate);
1154
1204
 
1155
1205
  // OPTION TO SKIP - PART 1
1156
- if (this.#customizationProcess !== customizationId) {
1157
- for (const r in this._stateEngine.renderingEngines)
1158
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
1159
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
1160
-
1161
- const eventCancel1: ITaskEvent = { type: eventType, id: eventId, progress: taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 1 + taskEventInfo.progressRange.min : 1, data: eventData, status: 'Output updating was exceeded by other customization request' };
1162
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1);
1163
- this._logger.debug(`Session(${this.id}).updateOutputs: Output updating was exceeded by other request.`);
1164
- return newNode;
1165
- } else if (this._closed === true) {
1166
- for (const r in this._stateEngine.renderingEngines)
1167
- if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
1168
- this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
1169
-
1170
- this._logger.debug(`Session(${this.id}).customize: Session was closed during customization request.`);
1206
+ const cancelResult = this.cancelProcess(customizationId, eventId, eventType, taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 1 + taskEventInfo.progressRange.min : 1, eventData, newNode);
1207
+ if (cancelResult) return cancelResult;
1208
+
1209
+ // call the update callbacks
1210
+ if (waitForViewportUpdate === false) {
1211
+ for (const outputId in this.outputs) {
1212
+ if (oldOutputVersions[outputId] !== newOutputVersions[outputId]) {
1213
+ this._eventEngine.emitEvent(EVENTTYPE.OUTPUT.OUTPUT_UPDATED, {
1214
+ outputId: outputId,
1215
+ outputVersion: newOutputVersions[outputId],
1216
+ newNode: newNode.children.find(c => c.name === outputId)!,
1217
+ oldNode: oldNode.children.find(c => c.name === outputId)!
1218
+ });
1219
+ }
1220
+ }
1221
+
1222
+ await this.waitForUpdateCallbacks(newOutputVersions, oldOutputVersions, newNode, oldNode);
1171
1223
 
1172
- const eventCancel1a: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'Session was closed during customization request' };
1173
- this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel1a);
1174
- return new SessionTreeNode();
1224
+ // OPTION TO SKIP - PART 2
1225
+ const cancelResult = this.cancelProcess(customizationId, eventId, eventType, taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 1 + taskEventInfo.progressRange.min : 1, eventData, newNode);
1226
+ if (cancelResult) return cancelResult;
1175
1227
  }
1176
1228
 
1177
1229
  if (this.automaticSceneUpdate) this.removeFromSceneTree(this.node);
@@ -1207,20 +1259,28 @@ export class SessionEngine implements ISessionEngine {
1207
1259
  }
1208
1260
 
1209
1261
  // update the viewports
1210
- if (waitForViewportUpdate)
1262
+ if (waitForViewportUpdate) {
1211
1263
  for (const r in this._stateEngine.renderingEngines)
1212
1264
  if (!this.excludeViewports.includes(this._stateEngine.renderingEngines[r].id))
1213
1265
  this._stateEngine.renderingEngines[r].update(`SessionEngine(${this.id}).updateOutputs`);
1214
1266
 
1215
- // call the update callback function on the session
1216
- if (this._updateCallback) this._updateCallback(newNode, oldNode);
1267
+ for (const outputId in this.outputs) {
1268
+ if (oldOutputVersions[outputId] !== newOutputVersions[outputId]) {
1269
+ this._eventEngine.emitEvent(EVENTTYPE.OUTPUT.OUTPUT_UPDATED, {
1270
+ outputId: outputId,
1271
+ outputVersion: newOutputVersions[outputId],
1272
+ newNode: newNode.children.find(c => c.name === outputId)!,
1273
+ oldNode: oldNode.children.find(c => c.name === outputId)!
1274
+ });
1275
+ }
1276
+ }
1217
1277
 
1218
- // call the update callback functions on the outputs
1219
- for (const outputId in this.outputs)
1220
- this.outputs[outputId].triggerUpdateCallback(
1221
- newNode.children.find(c => c.name === outputId)!,
1222
- oldNode.children.find(c => c.name === outputId)!
1223
- );
1278
+ await this.waitForUpdateCallbacks(newOutputVersions, oldOutputVersions, newNode, oldNode);
1279
+
1280
+ // OPTION TO SKIP - PART 3
1281
+ const cancelResult = this.cancelProcess(customizationId, eventId, eventType, taskEventInfo ? (taskEventInfo.progressRange.max - taskEventInfo.progressRange.min) * 1 + taskEventInfo.progressRange.min : 1, eventData, newNode);
1282
+ if (cancelResult) return cancelResult;
1283
+ }
1224
1284
 
1225
1285
  return this.node;
1226
1286
  }
@@ -1258,9 +1318,9 @@ export class SessionEngine implements ISessionEngine {
1258
1318
  }
1259
1319
  }
1260
1320
 
1261
- // #endregion Public Methods (25)
1321
+ // #endregion Public Methods (27)
1262
1322
 
1263
- // #region Private Methods (11)
1323
+ // #region Private Methods (13)
1264
1324
 
1265
1325
  private _saveSessionSettings() {
1266
1326
  const parameters = this.parameters;
@@ -1337,6 +1397,35 @@ export class SessionEngine implements ISessionEngine {
1337
1397
  this._sceneTree.root.updateVersion();
1338
1398
  }
1339
1399
 
1400
+ private cancelProcess(customizationId: string, eventId: string, eventType: TASK_TYPE, eventProgress: number, eventData: unknown, newNode: ITreeNode = new SessionTreeNode()): ITreeNode | undefined {
1401
+ if (this.#customizationProcess !== customizationId) {
1402
+ for (const r in this._stateEngine.renderingEngines)
1403
+ if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
1404
+ this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
1405
+
1406
+ const eventCancel: ITaskEvent = {
1407
+ type: eventType,
1408
+ id: eventId,
1409
+ progress: eventProgress,
1410
+ data: eventData,
1411
+ status: 'The request was exceeded by another customization request'
1412
+ };
1413
+ this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel);
1414
+ this._logger.debug(`Session(${this.id}).cancelProcess: The request was was exceeded by another request.`);
1415
+ return newNode;
1416
+ } else if ((this._closed as boolean) === true) {
1417
+ for (const r in this._stateEngine.renderingEngines)
1418
+ if (this._stateEngine.renderingEngines[r].busy.includes(customizationId))
1419
+ this._stateEngine.renderingEngines[r].busy.splice(this._stateEngine.renderingEngines[r].busy.indexOf(customizationId), 1);
1420
+
1421
+ this._logger.debug(`Session(${this.id}).cancelProcess: The session was closed during the request.`);
1422
+
1423
+ const eventCancel: ITaskEvent = { type: TASK_TYPE.SESSION_CUSTOMIZATION, id: eventId, progress: 1, data: { sessionId: this.id }, status: 'The session was closed during the request.' };
1424
+ this._eventEngine.emitEvent(EVENTTYPE.TASK.TASK_CANCEL, eventCancel);
1425
+ return new SessionTreeNode();
1426
+ }
1427
+ }
1428
+
1340
1429
  private checkAvailability(action?: string, checkForModelId = false) {
1341
1430
  if (!this._responseDto)
1342
1431
  throw new ShapeDiverViewerSessionError('Session.checkAvailability: responseDto not available.');
@@ -1385,22 +1474,33 @@ export class SessionEngine implements ISessionEngine {
1385
1474
  }
1386
1475
 
1387
1476
  private async customizeInternal(cancelRequest: () => boolean, taskEventInfo: OutputLoaderTaskEventInfo): Promise<ISessionTreeNode> {
1388
- return this.customizeSession(this._parameterValues, cancelRequest, taskEventInfo);
1477
+ return this.customizeSession(this._parameterValues, cancelRequest, taskEventInfo) as Promise<ISessionTreeNode>;
1389
1478
  }
1390
1479
 
1391
- private async customizeSession(parameters: { [key: string]: string }, cancelRequest: () => boolean, taskEventInfo: OutputLoaderTaskEventInfo, parallel = false, retry = false): Promise<ISessionTreeNode> {
1480
+ private async customizeSession(parameters: { [key: string]: string }, cancelRequest: () => boolean, taskEventInfo: OutputLoaderTaskEventInfo, parallel = false, loadOutputs = true, retry = false): Promise<ISessionTreeNode | ShapeDiverResponseDto> {
1392
1481
  this.checkAvailability('customize');
1393
1482
  try {
1394
1483
  this._performanceEvaluator.startSection('sessionResponse');
1395
1484
  const responseDto = await this._sdk.utils.submitAndWaitForCustomization(this._sdk, this._sessionId!, parameters);
1396
1485
  this._performanceEvaluator.endSection('sessionResponse');
1397
- if (cancelRequest()) return new SessionTreeNode();
1398
- if (parallel === false) this.updateResponseDto(responseDto);
1399
- return parallel === false ? this.loadOutputs(cancelRequest, taskEventInfo) : this.loadOutputsParallel(responseDto, cancelRequest, taskEventInfo);
1486
+ if (loadOutputs === true) {
1487
+ if (cancelRequest()) return new SessionTreeNode();
1488
+ if (parallel === true) {
1489
+ // special case, we load the outputs put don't add them to the scene
1490
+ return this.loadOutputsParallel(responseDto, cancelRequest, taskEventInfo);
1491
+ } else {
1492
+ // default case, we load the outputs and return the nodes
1493
+ this.updateResponseDto(responseDto);
1494
+ return this.loadOutputs(cancelRequest, taskEventInfo);
1495
+ }
1496
+ } else {
1497
+ // special case, we don't load the outputs and only return the responseDto
1498
+ return responseDto;
1499
+ }
1400
1500
  } catch (e) {
1401
1501
  await this.handleError(e, retry);
1402
1502
  if (cancelRequest()) return new SessionTreeNode();
1403
- return await this.customizeSession(parameters, cancelRequest, taskEventInfo, parallel, true);
1503
+ return await this.customizeSession(parameters, cancelRequest, taskEventInfo, parallel, loadOutputs, true);
1404
1504
  }
1405
1505
  }
1406
1506
 
@@ -1585,5 +1685,24 @@ export class SessionEngine implements ISessionEngine {
1585
1685
  }
1586
1686
  }
1587
1687
 
1588
- // #endregion Private Methods (11)
1688
+ private async waitForUpdateCallbacks(newOutputVersions: { [key: string]: string }, oldOutputVersions: { [key: string]: string }, newNode: ITreeNode, oldNode: ITreeNode) {
1689
+ // call the update callback function on the session
1690
+ if (this._updateCallback) await Promise.resolve(this._updateCallback(newNode, oldNode));
1691
+
1692
+ const promises = [];
1693
+ // call the update callback functions on the outputs
1694
+ for (const outputId in this.outputs) {
1695
+ if (oldOutputVersions[outputId] !== newOutputVersions[outputId]) {
1696
+ promises.push(
1697
+ this.outputs[outputId].triggerUpdateCallback(
1698
+ newNode.children.find(c => c.name === outputId)!,
1699
+ oldNode.children.find(c => c.name === outputId)!
1700
+ )
1701
+ );
1702
+ }
1703
+ }
1704
+ await Promise.all(promises);
1705
+ }
1706
+
1707
+ // #endregion Private Methods (13)
1589
1708
  }
@@ -173,8 +173,8 @@ export class Output implements IOutput {
173
173
 
174
174
  // #region Public Methods (4)
175
175
 
176
- public triggerUpdateCallback(newNode?: TreeNode, oldNode?: TreeNode) {
177
- if (this.#updateCallback) this.#updateCallback(newNode, oldNode);
176
+ public async triggerUpdateCallback(newNode?: TreeNode, oldNode?: TreeNode) {
177
+ if (this.#updateCallback) await Promise.resolve(this.#updateCallback(newNode, oldNode));
178
178
  }
179
179
 
180
180
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -130,11 +130,12 @@ export interface ISessionEngine {
130
130
  canGoBack(): boolean;
131
131
  canGoForward(): boolean;
132
132
  close(): Promise<void>;
133
- customize(force: boolean, waitForViewportUpdate?: boolean): Promise<ITreeNode>;
134
- customizeParallel(parameterValues: { [key: string]: string }): Promise<ITreeNode>;
133
+ customize(force: boolean, waitForViewportUpdate?: boolean):Promise<ITreeNode | ShapeDiverResponseDto>;
134
+ customizeParallel(parameterValues: { [key: string]: string }, loadOutputs: boolean): Promise<ITreeNode | ShapeDiverResponseDto>;
135
135
  goBack(): Promise<ITreeNode>;
136
136
  goForward(): Promise<ITreeNode>;
137
137
  init(parameterValues?: { [key: string]: string; }): Promise<void>;
138
+ loadCachedOutputsParallel(outputMapping: { [key: string]: string }, taskEventInfo?: OutputLoaderTaskEventInfo, retry?: boolean): Promise<{ [key: string]: ITreeNode | undefined }>;
138
139
  loadOutputs(cancelRequest: () => boolean, taskEventInfo: OutputLoaderTaskEventInfo): Promise<ITreeNode>;
139
140
  loadOutputsParallel(responseDto: ShapeDiverResponseDto, cancelRequest: () => boolean, taskEventInfo: OutputLoaderTaskEventInfo): Promise<ITreeNode>;
140
141
  requestExport(exportId: string, parameters: ShapeDiverRequestCustomization, maxWaitTime: number): Promise<ShapeDiverResponseExport>;