@jseeio/jsee 0.3.7 → 0.3.8

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/src/main.js CHANGED
@@ -52,19 +52,7 @@ function clone (obj) {
52
52
 
53
53
 
54
54
 
55
- function getName (code) {
56
- switch (typeof code) {
57
- case 'function':
58
- return code.name
59
- case 'string':
60
- const words = code.split(' ')
61
- const functionIndex = words.findIndex((word) => word == 'function')
62
- const name = words[functionIndex + 1]
63
- return name.includes('(') ? undefined : name
64
- default:
65
- return undefined
66
- }
67
- }
55
+ const getName = utils.getName
68
56
 
69
57
  // Return input value
70
58
  function getValue (input) {
@@ -112,6 +100,25 @@ function getInputs (model) {
112
100
  return []
113
101
  }
114
102
 
103
+ function collectStreamInputConfig (inputs, config={}) {
104
+ if (!Array.isArray(inputs)) {
105
+ return config
106
+ }
107
+ inputs.forEach(input => {
108
+ if (!isObject(input)) {
109
+ return
110
+ }
111
+ if (input.type === 'group') {
112
+ collectStreamInputConfig(input.elements, config)
113
+ return
114
+ }
115
+ if (input.type === 'file' && input.stream === true && input.name) {
116
+ config[input.name] = { stream: true }
117
+ }
118
+ })
119
+ return config
120
+ }
121
+
115
122
  function getFunctionContainer (target) {
116
123
  // Check if the number of parameters is > 1, then 'args'
117
124
  }
@@ -138,6 +145,8 @@ export default class JSEE {
138
145
  this.schema = params.schema || params.config // Previous naming
139
146
  this.utils = utils
140
147
  this.__version__ = VERSION
148
+ this.cancelled = false
149
+ this._cancelWorkerRun = null
141
150
 
142
151
  // Check if schema is provided
143
152
  if (typeof this.schema === 'undefined') {
@@ -168,35 +177,83 @@ export default class JSEE {
168
177
  notyf.success(txt)
169
178
  }
170
179
 
180
+ cancelCurrentRun () {
181
+ log('Stopping current run')
182
+ this.cancelled = true
183
+ if (typeof this._cancelWorkerRun === 'function') {
184
+ this._cancelWorkerRun()
185
+ }
186
+ }
187
+
188
+ isCancelled () {
189
+ return this.cancelled === true
190
+ }
191
+
171
192
  progress (i) {
193
+ const progressState = utils.getProgressState(i)
194
+ if (!progressState) {
195
+ return
196
+ }
197
+
172
198
  // Check if progress div is defined
173
199
  let progress = document.querySelector('#progress')
200
+ if (!progress && progressState.mode === 'determinate' && progressState.value === 0) {
201
+ return
202
+ }
203
+
174
204
  if (!progress) {
175
205
  progress = document.createElement('div')
176
206
  progress.setAttribute('id', 'progress')
177
207
  progress.style = 'position: fixed; top: 0; left: 0; width: 0; height: 3px; background: #00d1b2; z-index: 1000;'
178
208
  document.body.appendChild(progress)
179
209
  }
180
- progress.style.width = `${i}%`
210
+
211
+ let progressStyle = document.querySelector('#jsee-progress-style')
212
+ if (!progressStyle) {
213
+ progressStyle = document.createElement('style')
214
+ progressStyle.setAttribute('id', 'jsee-progress-style')
215
+ progressStyle.textContent = `
216
+ @keyframes jsee-progress-indeterminate {
217
+ 0% { transform: translateX(-120%); }
218
+ 100% { transform: translateX(360%); }
219
+ }
220
+ `
221
+ document.head.appendChild(progressStyle)
222
+ }
223
+
224
+ if (progressState.mode === 'indeterminate') {
225
+ progress.style.width = '30%'
226
+ progress.style.animation = 'jsee-progress-indeterminate 1.2s ease-in-out infinite'
227
+ } else {
228
+ progress.style.animation = 'none'
229
+ progress.style.transform = 'none'
230
+ progress.style.width = `${progressState.value}%`
231
+ }
181
232
  }
182
233
 
183
234
  async init () {
184
235
  // At this point this.schema is defined but can be in different forms (e.g. string, object, function)
185
- await this.initSchema() // -> this.schema (object)
186
- await this.initModel()
187
- await this.initInputs()
188
- await this.initVue() // -> this.app, this.data
189
- await this.initPipeline()
190
- if (this.schema.autorun) {
191
- log('Autorun is enabled. Running the model')
192
- this.run('init')
236
+ await this.initSchema() // Inits: this.schema (object)
237
+ await this.initModel() // Inits: this.model (array of objects)
238
+ await this.initInputs() // Inits: schema inputs based on url
239
+ this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
240
+ await this.initVue() // Inits: this.app, this.data
241
+ await this.initPipeline() // Inits: this.pipeline (function)
242
+ if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
243
+ // 1. If autorun is enabled in the schema, run the model immediately
244
+ // 2. Server-side inputs: If there are inputs with disabled and reactive flags
245
+ // (we assume that they are set by the server and trigger the model run)
246
+ log('[Init] First run of the model due to autorun or reactive inputs')
247
+ // Catch here to prevent unhandled rejection from init-time run
248
+ this.run('init').catch(err => log('Init run error:', err))
193
249
  }
194
250
  }
195
251
 
196
252
  async initSchema () {
197
253
  // Check if schema is a string (url to json)
198
254
  if (typeof this.schema === 'string') {
199
- this.schemaUrl = this.schema.indexOf('json') ? this.schema : this.schema + '.json'
255
+ // indexOf returns -1 (truthy) when not found, so use includes instead
256
+ this.schemaUrl = this.schema.includes('.json') ? this.schema : this.schema + '.json'
200
257
 
201
258
  // Check if schema is present in the hidden DOM element
202
259
  const schema = utils.loadFromDOM(this.schemaUrl)
@@ -227,6 +284,17 @@ export default class JSEE {
227
284
  notyf.error('Schema is in a wrong format')
228
285
  throw new Error(`Schema is in a wrong format: ${this.schema}`)
229
286
  }
287
+
288
+ // Validate schema shape early so init stages fail fast on critical issues
289
+ const schemaValidation = utils.validateSchema(this.schema)
290
+ schemaValidation.warnings.forEach(warning => {
291
+ log('[Schema validation warning]', warning)
292
+ })
293
+ if (schemaValidation.errors.length) {
294
+ const firstError = schemaValidation.errors[0]
295
+ notyf.error(firstError)
296
+ throw new Error(`Schema validation failed: ${schemaValidation.errors.join('; ')}`)
297
+ }
230
298
  }
231
299
 
232
300
  async initModel () {
@@ -234,8 +302,21 @@ export default class JSEE {
234
302
  // At the end it should be an array of objects that define a sequence of tasks
235
303
  this.model = []
236
304
 
305
+ // Check if there's a render or view defined in the schema
306
+ let view = this.schema.render || this.schema.view
307
+ if (isObject(view)) {
308
+ // If view is an object, convert it to an array
309
+ view = [view] // Convert to array if it's an object
310
+ }
311
+ if (Array.isArray(view)) {
312
+ view.forEach(v => {
313
+ v.worker = false // Render should not be in a worker
314
+ })
315
+ log('View is defined in the schema')
316
+ }
317
+
237
318
  // Check if model is a function (model)
238
- ;[this.schema.model, this.schema.render].forEach(m => {
319
+ ;[this.schema.model, view].forEach(m => {
239
320
  // Function -> {code: Function}
240
321
  if (typeof m === 'function') {
241
322
  this.model.push({
@@ -362,6 +443,8 @@ export default class JSEE {
362
443
  let paramValue = null
363
444
  if (urlParams.has(input.name)) {
364
445
  paramValue = urlParams.get(input.name);
446
+ } else if (urlParams.has(utils.sanitizeName(input.name))) {
447
+ paramValue = urlParams.get(utils.sanitizeName(input.name));
365
448
  } else if (input.alias) {
366
449
  // Handle alias as either a string or an array of strings
367
450
  if (Array.isArray(input.alias)) {
@@ -379,24 +462,28 @@ export default class JSEE {
379
462
 
380
463
  // Set input value from URL param with type conversion
381
464
  if (paramValue !== null) {
382
- switch (input.type) {
383
- case 'number':
384
- paramValue = Number(paramValue);
385
- break;
386
- case 'boolean':
387
- paramValue = paramValue === 'true';
388
- break;
389
- case 'json':
390
- try {
391
- paramValue = JSON.parse(paramValue);
392
- } catch (e) {
393
- console.error(`Failed to parse JSON for input ${input.name}:`, e);
394
- }
395
- break;
396
- default:
397
- break;
465
+ if (input.type === 'file') {
466
+ input.url = paramValue;
467
+ } else {
468
+ switch (input.type) {
469
+ case 'number':
470
+ paramValue = Number(paramValue);
471
+ break;
472
+ case 'boolean':
473
+ paramValue = paramValue === 'true';
474
+ break;
475
+ case 'json':
476
+ try {
477
+ paramValue = JSON.parse(paramValue);
478
+ } catch (e) {
479
+ console.error(`Failed to parse JSON for input ${input.name}:`, e);
480
+ }
481
+ break;
482
+ default:
483
+ break;
484
+ }
485
+ input.default = paramValue
398
486
  }
399
- input.default = paramValue
400
487
  }
401
488
  })
402
489
  log('Inputs are:', this.schema.inputs)
@@ -414,17 +501,14 @@ export default class JSEE {
414
501
  this.modelContainer = container.querySelector('#model')
415
502
  // Init overlay
416
503
  this.overlay = new Overlay(this.inputsContainer ? this.inputsContainer : this.outputsContainer)
417
- // Add stop button to the overlay if interval is defined
418
- if (this.schema.interval) {
419
- this.stopElement = document.createElement('button')
420
- this.stopElement.innerHTML = 'Stop'
421
- this.stopElement.style = 'background: white; color: #333; border: 1px solid #DDD; padding: 10px; border-radius: 5px; cursor: pointer;'
422
- this.stopElement.addEventListener('click', () => {
423
- this.running = false
424
- })
425
- this.overlay.element.innerHTML = ''
426
- this.overlay.element.appendChild(this.stopElement)
427
- }
504
+ // Stop button is shown only while a run is active
505
+ this.stopElement = document.createElement('button')
506
+ this.stopElement.innerHTML = 'Stop'
507
+ this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
508
+ this.stopElement.addEventListener('click', () => {
509
+ this.cancelCurrentRun()
510
+ })
511
+ this.overlay.element.appendChild(this.stopElement)
428
512
  resolve()
429
513
  }, log)
430
514
  this.data = this.app.$data
@@ -439,15 +523,14 @@ export default class JSEE {
439
523
  this.pipeline = (inputs) => inputs
440
524
  // Async for-loop over this.model (again)
441
525
  for (const [i, m] of this.model.entries()) {
442
- log('Initilizing the pipeline with model:', i, m.type)
443
526
  let modelFunc
444
527
  if (m.worker) {
445
528
  // Init worker model
446
- log('Initializing model in a worker:', m.name || m.url)
529
+ log(`[Init pipeline] Initializing model ${i} in a worker: ${m.name || m.url}`)
447
530
  modelFunc = await this.initWorker(m)
448
531
  } else {
449
532
  // Init specific model types
450
- log('Initializing model in the main thread:', m.name || m.url)
533
+ log(`[Init pipeline] Initializing model ${i} in the main thread: ${m.name || m.url}`)
451
534
  switch (m.type) {
452
535
  case 'py':
453
536
  modelFunc = await this.initPython(m)
@@ -470,20 +553,40 @@ export default class JSEE {
470
553
  notyf.error('No type information')
471
554
  throw new Error(`No type information: ${m.type}`)
472
555
  }
556
+
557
+ const streamInputConfig = this.streamInputConfig || {}
558
+ const hasStreamInputs = Object.keys(streamInputConfig).length > 0
559
+ if (hasStreamInputs) {
560
+ const originalModelFunc = modelFunc
561
+ modelFunc = (inputs) => {
562
+ const wrappedInputs = utils.wrapStreamInputs(inputs, streamInputConfig, {
563
+ isCancelled: () => this.isCancelled(),
564
+ onProgress: (value) => this.progress(value)
565
+ })
566
+ return originalModelFunc(wrappedInputs)
567
+ }
568
+ }
473
569
  }
474
570
 
475
571
  this.pipeline = (p => {
476
572
  return async (inputs) => {
477
573
  const resPrev = await p(inputs)
574
+ // Early stop if resPrev is object and has stop flag
575
+ if (isObject(resPrev) && resPrev.stop) {
576
+ log('[Pipeline] Stopping the pipeline due to stop flag in the result')
577
+ return resPrev
578
+ }
478
579
  const resNext = await modelFunc(resPrev)
479
580
  if (isObject(resNext) && isObject(resPrev)) {
480
581
  // If both results are objects, merge them
582
+ log(`[Pipeline] Merging results: ${Object.keys(resPrev).join(', ')} + ${Object.keys(resNext).join(', ')}`)
481
583
  return Object.assign({}, resPrev, resNext)
482
584
  } else if (typeof resNext !== 'undefined') {
483
585
  // If next result is defined, return it
484
586
  return resNext
485
587
  } else {
486
588
  // Otherwise return previous result (pass through)
589
+ log('[Pipeline] Passing through the previous result')
487
590
  return resPrev
488
591
  }
489
592
  }
@@ -510,43 +613,81 @@ export default class JSEE {
510
613
  model.name = 'anon'
511
614
  }
512
615
 
513
- const modelFunc = (inputs) => new Promise((resolve, reject) => {
514
- worker.onmessage = (e) => {
515
- const res = e.data
516
- if ((typeof res === 'object') && (res._status)) {
517
- switch (res._status) {
518
- case 'loaded':
519
- notyf.success('Loaded model (in worker)')
520
- log('Loaded model (in worker):', res)
521
- resolve(res)
522
- break
523
- case 'log':
524
- log(...res._log)
525
- break
526
- case 'progress':
527
- this.progress(res._progress)
528
- break
529
- case 'error':
530
- notyf.error(res._error)
531
- log('Error from worker:', res._error)
532
- reject(res._error)
533
- break
616
+ // Timeout prevents permanently frozen UI if worker hangs (default 30s, configurable via model.timeout)
617
+ const timeoutMs = model.timeout || 30000
618
+ this._cancelWorkerRun = () => worker.postMessage({ _cmd: 'cancel' })
619
+
620
+ const modelFunc = (inputs) => {
621
+ const isInitCall = inputs && inputs.code !== undefined
622
+ const payload = isInitCall
623
+ ? inputs
624
+ : utils.toWorkerSerializable(inputs)
625
+
626
+ const workerPromise = new Promise((resolve, reject) => {
627
+ worker.onmessage = (e) => {
628
+ const res = e.data
629
+ if ((typeof res === 'object') && (res._status)) {
630
+ switch (res._status) {
631
+ case 'loaded':
632
+ notyf.success('Loaded model (in worker)')
633
+ log('Loaded model (in worker):', res)
634
+ this.progress(0)
635
+ resolve(res)
636
+ break
637
+ case 'log':
638
+ log(...res._log)
639
+ break
640
+ case 'progress':
641
+ this.progress(res._progress)
642
+ break
643
+ case 'error':
644
+ notyf.error(res._error)
645
+ log('Error from worker:', res._error)
646
+ this.progress(0)
647
+ reject(res._error)
648
+ break
649
+ }
650
+ } else {
651
+ log('Response from worker:', res)
652
+ this.progress(0)
653
+ resolve(res)
534
654
  }
535
- } else {
536
- log('Response from worker:', res)
537
- resolve(res)
538
655
  }
539
- }
540
- worker.onerror = (e) => {
541
- notyf.error(e.message)
542
- log('Error from worker:', e)
543
- reject(e)
544
- }
545
- worker.postMessage(inputs)
546
- })
656
+ worker.onerror = (e) => {
657
+ notyf.error(e.message)
658
+ log('Error from worker:', e)
659
+ this.progress(0)
660
+ reject(e)
661
+ }
662
+ try {
663
+ worker.postMessage(payload)
664
+ } catch (error) {
665
+ try {
666
+ const fallbackPayload = JSON.parse(JSON.stringify(payload))
667
+ worker.postMessage(fallbackPayload)
668
+ } catch (fallbackError) {
669
+ reject(fallbackError)
670
+ }
671
+ }
672
+ })
673
+
674
+ // Skip timeout for init call (loading model can be slow); apply to execution calls
675
+ if (isInitCall) return workerPromise
547
676
 
548
- // Initial worker call with model definition
549
- await modelFunc(model)
677
+ const timeoutPromise = new Promise((_, reject) => {
678
+ setTimeout(() => {
679
+ worker.terminate()
680
+ reject(new Error(`Worker timed out after ${timeoutMs}ms`))
681
+ }, timeoutMs)
682
+ })
683
+ return Promise.race([workerPromise, timeoutPromise])
684
+ }
685
+
686
+ // Initial worker call with model definition and stream input config
687
+ const modelInitPayload = Object.assign({}, model, {
688
+ _streamInputConfig: this.streamInputConfig || {}
689
+ })
690
+ await modelFunc(modelInitPayload)
550
691
 
551
692
  // Worker will be in the context of each modelFunc
552
693
  return modelFunc
@@ -640,44 +781,76 @@ export default class JSEE {
640
781
  // 1. custom input button name
641
782
  // 2. `run`
642
783
  // 3. `autorun`
784
+
785
+ // Prevent overlapping runs: autorun skips, manual clicks queue
786
+ // Prevent overlapping runs: reactive/autorun calls are dropped, manual clicks queue
787
+ if (this.running) {
788
+ if (caller === 'autorun' || caller === 'reactive') return
789
+ log('Run already in progress, queuing', caller)
790
+ this._pendingRun = caller
791
+ return
792
+ }
793
+
643
794
  const schema = this.schema
644
795
  const data = this.data
645
796
  this.running = true
797
+ this.cancelled = false
798
+ // Run token to detect stale results when worker.onmessage gets rebound
799
+ const runToken = this._runToken = {}
646
800
 
647
- log('Running the pipeline...')
648
- // Collect input values
649
- let inputValues = {}
650
- data.inputs.forEach(input => {
651
- // Skip buttons
652
- if (input.name && !(input.type == 'action' || input.type == 'button')) {
653
- inputValues[input.name] = getValue(input)
654
- }
655
- })
656
- // Add caller to input values so we can change model behavior based on it
657
- inputValues.caller = caller
801
+ try {
802
+ log('Running the pipeline...')
803
+ // Collect input values
804
+ let inputValues = {}
805
+ data.inputs.forEach(input => {
806
+ // Skip buttons
807
+ if (input.name && !(input.type == 'action' || input.type == 'button')) {
808
+ inputValues[input.name] = getValue(input)
809
+ }
810
+ })
811
+ // Add caller to input values so we can change model behavior based on it
812
+ inputValues.caller = caller
658
813
 
659
- log('Input values:', inputValues)
660
- // We have all input values here, pass them to worker, window.modelFunc or tf
661
- if (!schema.model.autorun) {
814
+ log('Input values:', inputValues)
662
815
  this.overlay.show()
663
- }
816
+ if (this.stopElement) {
817
+ this.stopElement.style.display = 'inline-block'
818
+ }
664
819
 
665
- // Run pipeline
666
- const results = await this.pipeline(inputValues)
820
+ // Run pipeline
821
+ const results = await this.pipeline(inputValues)
667
822
 
668
- // Output results
669
- this.output(results)
823
+ // Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
824
+ if (this._runToken !== runToken) return
670
825
 
671
- // Check if interval is defined
672
- if (schema.interval && this.running && (caller === 'run')) {
673
- log('Interval is defined:', schema.interval)
674
- await utils.delay(schema.interval)
675
- await this.run(caller)
676
- }
826
+ // Output results
827
+ this.output(results)
677
828
 
678
- // Hide overlay
679
- this.overlay.hide()
680
- return
829
+ // Check if interval is defined
830
+ if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
831
+ log('Interval is defined:', schema.interval)
832
+ await utils.delay(schema.interval)
833
+ await this.run(caller)
834
+ }
835
+ } catch (err) {
836
+ // Surface pipeline/worker errors so they don't silently swallow failures
837
+ log('Pipeline error:', err)
838
+ notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
839
+ } finally {
840
+ // Always clean up UI state so overlay and running flag don't get stuck
841
+ this.overlay.hide()
842
+ if (this.stopElement) {
843
+ this.stopElement.style.display = 'none'
844
+ }
845
+ this.running = false
846
+
847
+ // Drain queued run if a manual click arrived while we were running
848
+ if (this._pendingRun) {
849
+ const pending = this._pendingRun
850
+ this._pendingRun = null
851
+ this.run(pending).catch(err => log('Queued run error:', err))
852
+ }
853
+ }
681
854
  }
682
855
 
683
856
  async outputAsync (res) {
@@ -695,28 +868,65 @@ export default class JSEE {
695
868
 
696
869
  log('[Output] Got output results of type:', typeof res)
697
870
 
698
- // Process results (res)
699
- const inputNames = this.schema.inputs.map(i => i.name)
700
- if (isObject(res) && Object.keys(res).every(key => inputNames.includes(key))) {
701
- // Update inputs from results
702
- log('Updating inputs:', Object.keys(res))
703
- this.data.inputs.forEach((input, i) => {
704
- if (input.name && (typeof res[input.name] !== 'undefined')) {
705
- log('Updating input: ', input.name, 'with data:', res[input.name])
706
- const r = res[input.name]
707
- if (typeof r === 'object') {
708
- Object.keys(r).forEach(k => {
709
- input[k] = r[k]
710
- })
711
- } else {
712
- input.value = r
871
+ const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
872
+ log('Input names:', inputNames)
873
+
874
+ if (isObject(res)) {
875
+ // Drop system fields
876
+ delete res.caller
877
+ delete res.stop
878
+ delete res._status
879
+ delete res._log
880
+ delete res._progress
881
+ log('Processing results as an object:', res)
882
+
883
+ if (Object.keys(res).every(key => inputNames.includes(key))) {
884
+ // Update input fields from results
885
+ // e.g. loading a csv file and updating list of target variables
886
+ // This will be dynamically updated in the UI
887
+ log('Updating inputs from results with keys:', Object.keys(res))
888
+ this.data.inputs.forEach((input, i) => {
889
+ if (input.name && (typeof res[input.name] !== 'undefined')) {
890
+ log(`Updating input: ${input.name} with data: ${res[input.name]}`)
891
+ const r = res[input.name]
892
+ if (typeof r === 'object') {
893
+ Object.keys(r).forEach(k => {
894
+ input[k] = r[k]
895
+ })
896
+ } else {
897
+ input.value = r
898
+ }
713
899
  }
714
- }
715
- })
716
- } else if (this.renderFunc) {
717
- // Pass results to a custom render function
718
- log('Calling a render function...')
719
- this.renderFunc(res, this)
900
+ })
901
+ } else if (this.data.outputs && this.data.outputs.length) {
902
+ // Update outputs from results
903
+ log('Updating outputs from results with keys:', Object.keys(res))
904
+ this.data.outputs.forEach((output, i) => {
905
+ // try output.name, sanitized output.name, output.alias
906
+ const r = res[output.name]
907
+ || res[utils.sanitizeName(output.name)]
908
+ || (output.alias && res[output.alias])
909
+ if (typeof r !== 'undefined') {
910
+ log(`Updating output: ${output.name} with data: ${typeof r}`)
911
+ output.value = r
912
+ }
913
+ })
914
+ } else if (!this.schema.render && !this.schema.view) {
915
+ // There's no render or view defined in the schema, also:
916
+ // No outputs defined, create outputs from results
917
+ log('Creating outputs from results with keys:', Object.keys(res))
918
+ this.data.outputs = Object.keys(res)
919
+ .filter(key => !inputNames.includes(key))
920
+ .filter(key => key !== 'caller') // Filter out caller
921
+ .map(key => {
922
+ return {
923
+ 'name': key,
924
+ // typeof returns 'object' for arrays; distinguish them for proper rendering
925
+ 'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
926
+ 'value': res[key]
927
+ }
928
+ })
929
+ }
720
930
  } else if (Array.isArray(res) && res.length) {
721
931
  // Result is array
722
932
  if (this.data.outputs && this.data.outputs.length) {
@@ -737,20 +947,6 @@ export default class JSEE {
737
947
  'value': res
738
948
  }]
739
949
  }
740
- } else if (typeof res === 'object') {
741
- if (this.data.outputs && this.data.outputs.length) {
742
- this.data.outputs.forEach((output, i) => {
743
- if (output.name && (typeof res[output.name] !== 'undefined')) {
744
- log('Updating output: ', output.name)
745
- output.value = res[output.name]
746
- }
747
- })
748
- } else {
749
- this.data.outputs = [{
750
- 'type': 'object',
751
- 'value': res
752
- }]
753
- }
754
950
  } else if (this.schema.outputs && this.schema.outputs.length === 1) {
755
951
  // One output value passed as raw js object
756
952
  this.data.outputs[0].value = res