@jseeio/jsee 0.3.7 → 0.3.9

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
@@ -3,6 +3,7 @@ import Worker from './worker.js'
3
3
 
4
4
  const utils = require('./utils')
5
5
  const isObject = utils.isObject
6
+ const { DEFAULT_CONTAINER, DEFAULT_WORKER_TIMEOUT } = require('./constants')
6
7
 
7
8
  const { Notyf } = require('notyf')
8
9
  const notyf = new Notyf({
@@ -52,19 +53,7 @@ function clone (obj) {
52
53
 
53
54
 
54
55
 
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
- }
56
+ const getName = utils.getName
68
57
 
69
58
  // Return input value
70
59
  function getValue (input) {
@@ -80,9 +69,13 @@ function getValue (input) {
80
69
  }
81
70
 
82
71
  function getModelType (model) {
83
- if (model.code && typeof model.code === 'string' && model.code.split(' ').map(v => v.trim()).includes('def')) {
84
- return 'py'
85
- } else if (model.url) {
72
+ if (typeof model.code === 'string' && model.code.trim().length > 0) {
73
+ if (model.code.split(' ').map(v => v.trim()).includes('def')) {
74
+ return 'py'
75
+ }
76
+ return 'function'
77
+ }
78
+ if (model.url) {
86
79
  return 'post'
87
80
  }
88
81
  return 'function'
@@ -112,6 +105,25 @@ function getInputs (model) {
112
105
  return []
113
106
  }
114
107
 
108
+ function collectStreamInputConfig (inputs, config={}) {
109
+ if (!Array.isArray(inputs)) {
110
+ return config
111
+ }
112
+ inputs.forEach(input => {
113
+ if (!isObject(input)) {
114
+ return
115
+ }
116
+ if (input.type === 'group') {
117
+ collectStreamInputConfig(input.elements, config)
118
+ return
119
+ }
120
+ if (input.type === 'file' && input.stream === true && input.name) {
121
+ config[input.name] = { stream: true }
122
+ }
123
+ })
124
+ return config
125
+ }
126
+
115
127
  function getFunctionContainer (target) {
116
128
  // Check if the number of parameters is > 1, then 'args'
117
129
  }
@@ -138,6 +150,8 @@ export default class JSEE {
138
150
  this.schema = params.schema || params.config // Previous naming
139
151
  this.utils = utils
140
152
  this.__version__ = VERSION
153
+ this.cancelled = false
154
+ this._cancelWorkerRun = null
141
155
 
142
156
  // Check if schema is provided
143
157
  if (typeof this.schema === 'undefined') {
@@ -148,8 +162,8 @@ export default class JSEE {
148
162
  // Check if container is provided
149
163
  if (typeof this.container === 'undefined') {
150
164
  // Check if 'jsee-container' exists
151
- if (document.querySelector('#jsee-container')) {
152
- this.container = '#jsee-container'
165
+ if (document.querySelector(DEFAULT_CONTAINER)) {
166
+ this.container = DEFAULT_CONTAINER
153
167
  log(`Using default container: ${this.container}`)
154
168
  } else {
155
169
  notyf.error('No container provided')
@@ -168,35 +182,83 @@ export default class JSEE {
168
182
  notyf.success(txt)
169
183
  }
170
184
 
185
+ cancelCurrentRun () {
186
+ log('Stopping current run')
187
+ this.cancelled = true
188
+ if (typeof this._cancelWorkerRun === 'function') {
189
+ this._cancelWorkerRun()
190
+ }
191
+ }
192
+
193
+ isCancelled () {
194
+ return this.cancelled === true
195
+ }
196
+
171
197
  progress (i) {
198
+ const progressState = utils.getProgressState(i)
199
+ if (!progressState) {
200
+ return
201
+ }
202
+
172
203
  // Check if progress div is defined
173
204
  let progress = document.querySelector('#progress')
205
+ if (!progress && progressState.mode === 'determinate' && progressState.value === 0) {
206
+ return
207
+ }
208
+
174
209
  if (!progress) {
175
210
  progress = document.createElement('div')
176
211
  progress.setAttribute('id', 'progress')
177
212
  progress.style = 'position: fixed; top: 0; left: 0; width: 0; height: 3px; background: #00d1b2; z-index: 1000;'
178
213
  document.body.appendChild(progress)
179
214
  }
180
- progress.style.width = `${i}%`
215
+
216
+ let progressStyle = document.querySelector('#jsee-progress-style')
217
+ if (!progressStyle) {
218
+ progressStyle = document.createElement('style')
219
+ progressStyle.setAttribute('id', 'jsee-progress-style')
220
+ progressStyle.textContent = `
221
+ @keyframes jsee-progress-indeterminate {
222
+ 0% { transform: translateX(-120%); }
223
+ 100% { transform: translateX(360%); }
224
+ }
225
+ `
226
+ document.head.appendChild(progressStyle)
227
+ }
228
+
229
+ if (progressState.mode === 'indeterminate') {
230
+ progress.style.width = '30%'
231
+ progress.style.animation = 'jsee-progress-indeterminate 1.2s ease-in-out infinite'
232
+ } else {
233
+ progress.style.animation = 'none'
234
+ progress.style.transform = 'none'
235
+ progress.style.width = `${progressState.value}%`
236
+ }
181
237
  }
182
238
 
183
239
  async init () {
184
240
  // 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')
241
+ await this.initSchema() // Inits: this.schema (object)
242
+ await this.initModel() // Inits: this.model (array of objects)
243
+ await this.initInputs() // Inits: schema inputs based on url
244
+ this.streamInputConfig = collectStreamInputConfig(this.schema.inputs)
245
+ await this.initVue() // Inits: this.app, this.data
246
+ await this.initPipeline() // Inits: this.pipeline (function)
247
+ if (this.schema.autorun || this.schema.inputs.some(input => input.disabled && input.reactive)) {
248
+ // 1. If autorun is enabled in the schema, run the model immediately
249
+ // 2. Server-side inputs: If there are inputs with disabled and reactive flags
250
+ // (we assume that they are set by the server and trigger the model run)
251
+ log('[Init] First run of the model due to autorun or reactive inputs')
252
+ // Catch here to prevent unhandled rejection from init-time run
253
+ this.run('init').catch(err => log('Init run error:', err))
193
254
  }
194
255
  }
195
256
 
196
257
  async initSchema () {
197
258
  // Check if schema is a string (url to json)
198
259
  if (typeof this.schema === 'string') {
199
- this.schemaUrl = this.schema.indexOf('json') ? this.schema : this.schema + '.json'
260
+ // indexOf returns -1 (truthy) when not found, so use includes instead
261
+ this.schemaUrl = this.schema.includes('.json') ? this.schema : this.schema + '.json'
200
262
 
201
263
  // Check if schema is present in the hidden DOM element
202
264
  const schema = utils.loadFromDOM(this.schemaUrl)
@@ -227,6 +289,17 @@ export default class JSEE {
227
289
  notyf.error('Schema is in a wrong format')
228
290
  throw new Error(`Schema is in a wrong format: ${this.schema}`)
229
291
  }
292
+
293
+ // Validate schema shape early so init stages fail fast on critical issues
294
+ const schemaValidation = utils.validateSchema(this.schema)
295
+ schemaValidation.warnings.forEach(warning => {
296
+ log('[Schema validation warning]', warning)
297
+ })
298
+ if (schemaValidation.errors.length) {
299
+ const firstError = schemaValidation.errors[0]
300
+ notyf.error(firstError)
301
+ throw new Error(`Schema validation failed: ${schemaValidation.errors.join('; ')}`)
302
+ }
230
303
  }
231
304
 
232
305
  async initModel () {
@@ -234,8 +307,21 @@ export default class JSEE {
234
307
  // At the end it should be an array of objects that define a sequence of tasks
235
308
  this.model = []
236
309
 
310
+ // Check if there's a render or view defined in the schema
311
+ let view = this.schema.render || this.schema.view
312
+ if (isObject(view)) {
313
+ // If view is an object, convert it to an array
314
+ view = [view] // Convert to array if it's an object
315
+ }
316
+ if (Array.isArray(view)) {
317
+ view.forEach(v => {
318
+ v.worker = false // Render should not be in a worker
319
+ })
320
+ log('View is defined in the schema')
321
+ }
322
+
237
323
  // Check if model is a function (model)
238
- ;[this.schema.model, this.schema.render].forEach(m => {
324
+ ;[this.schema.model, view].forEach(m => {
239
325
  // Function -> {code: Function}
240
326
  if (typeof m === 'function') {
241
327
  this.model.push({
@@ -359,45 +445,16 @@ export default class JSEE {
359
445
  }
360
446
 
361
447
  // Get input value from URL params
362
- let paramValue = null
363
- if (urlParams.has(input.name)) {
364
- paramValue = urlParams.get(input.name);
365
- } else if (input.alias) {
366
- // Handle alias as either a string or an array of strings
367
- if (Array.isArray(input.alias)) {
368
- for (let alias of input.alias) {
369
- if (urlParams.has(alias)) {
370
- paramValue = urlParams.get(alias);
371
- break;
372
- }
373
- }
374
- } else if (typeof input.alias === 'string' && urlParams.has(input.alias)) {
375
- paramValue = urlParams.get(input.alias);
376
- }
377
- }
448
+ const paramValue = utils.getUrlParam(urlParams, input)
378
449
  log(`Param value for ${input.name}:`, paramValue)
379
450
 
380
- // Set input value from URL param with type conversion
381
- 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;
398
- }
399
- input.default = paramValue
451
+ if (paramValue === null) return
452
+ if (input.type === 'file') {
453
+ input.url = paramValue
454
+ input.urlAutoLoad = true
455
+ return
400
456
  }
457
+ input.default = utils.coerceParam(paramValue, input.type, input.name)
401
458
  })
402
459
  log('Inputs are:', this.schema.inputs)
403
460
  }
@@ -414,17 +471,14 @@ export default class JSEE {
414
471
  this.modelContainer = container.querySelector('#model')
415
472
  // Init overlay
416
473
  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
- }
474
+ // Stop button is shown only while a run is active
475
+ this.stopElement = document.createElement('button')
476
+ this.stopElement.innerHTML = 'Stop'
477
+ this.stopElement.style = 'display: none; margin-left: 12px; background: white; color: #333; border: 1px solid #DDD; padding: 6px 10px; border-radius: 5px; cursor: pointer;'
478
+ this.stopElement.addEventListener('click', () => {
479
+ this.cancelCurrentRun()
480
+ })
481
+ this.overlay.element.appendChild(this.stopElement)
428
482
  resolve()
429
483
  }, log)
430
484
  this.data = this.app.$data
@@ -439,15 +493,14 @@ export default class JSEE {
439
493
  this.pipeline = (inputs) => inputs
440
494
  // Async for-loop over this.model (again)
441
495
  for (const [i, m] of this.model.entries()) {
442
- log('Initilizing the pipeline with model:', i, m.type)
443
496
  let modelFunc
444
497
  if (m.worker) {
445
498
  // Init worker model
446
- log('Initializing model in a worker:', m.name || m.url)
499
+ log(`[Init pipeline] Initializing model ${i} in a worker: ${m.name || m.url}`)
447
500
  modelFunc = await this.initWorker(m)
448
501
  } else {
449
502
  // Init specific model types
450
- log('Initializing model in the main thread:', m.name || m.url)
503
+ log(`[Init pipeline] Initializing model ${i} in the main thread: ${m.name || m.url}`)
451
504
  switch (m.type) {
452
505
  case 'py':
453
506
  modelFunc = await this.initPython(m)
@@ -470,20 +523,40 @@ export default class JSEE {
470
523
  notyf.error('No type information')
471
524
  throw new Error(`No type information: ${m.type}`)
472
525
  }
526
+
527
+ const streamInputConfig = this.streamInputConfig || {}
528
+ const hasStreamInputs = Object.keys(streamInputConfig).length > 0
529
+ if (hasStreamInputs) {
530
+ const originalModelFunc = modelFunc
531
+ modelFunc = (inputs) => {
532
+ const wrappedInputs = utils.wrapStreamInputs(inputs, streamInputConfig, {
533
+ isCancelled: () => this.isCancelled(),
534
+ onProgress: (value) => this.progress(value)
535
+ })
536
+ return originalModelFunc(wrappedInputs)
537
+ }
538
+ }
473
539
  }
474
540
 
475
541
  this.pipeline = (p => {
476
542
  return async (inputs) => {
477
543
  const resPrev = await p(inputs)
544
+ // Early stop if resPrev is object and has stop flag
545
+ if (isObject(resPrev) && resPrev.stop) {
546
+ log('[Pipeline] Stopping the pipeline due to stop flag in the result')
547
+ return resPrev
548
+ }
478
549
  const resNext = await modelFunc(resPrev)
479
550
  if (isObject(resNext) && isObject(resPrev)) {
480
551
  // If both results are objects, merge them
552
+ log(`[Pipeline] Merging results: ${Object.keys(resPrev).join(', ')} + ${Object.keys(resNext).join(', ')}`)
481
553
  return Object.assign({}, resPrev, resNext)
482
554
  } else if (typeof resNext !== 'undefined') {
483
555
  // If next result is defined, return it
484
556
  return resNext
485
557
  } else {
486
558
  // Otherwise return previous result (pass through)
559
+ log('[Pipeline] Passing through the previous result')
487
560
  return resPrev
488
561
  }
489
562
  }
@@ -510,43 +583,89 @@ export default class JSEE {
510
583
  model.name = 'anon'
511
584
  }
512
585
 
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
586
+ // Timeout prevents permanently frozen UI if worker hangs (default 30s, configurable via model.timeout)
587
+ const timeoutMs = model.timeout || DEFAULT_WORKER_TIMEOUT
588
+ this._cancelWorkerRun = () => worker.postMessage({ _cmd: 'cancel' })
589
+
590
+ const modelFunc = (inputs) => {
591
+ const isInitCall = inputs && inputs.code !== undefined
592
+ const payload = isInitCall
593
+ ? inputs
594
+ : utils.toWorkerSerializable(inputs)
595
+
596
+ const workerPromise = new Promise((resolve, reject) => {
597
+ worker.onmessage = (e) => {
598
+ const res = e.data
599
+ if ((typeof res === 'object') && (res._status)) {
600
+ switch (res._status) {
601
+ case 'loaded':
602
+ notyf.success('Loaded model (in worker)')
603
+ log('Loaded model (in worker):', res)
604
+ this.progress(0)
605
+ resolve(res)
606
+ break
607
+ case 'log':
608
+ log(...res._log)
609
+ break
610
+ case 'progress':
611
+ this.progress(res._progress)
612
+ break
613
+ case 'error':
614
+ notyf.error(res._error)
615
+ log('Error from worker:', res._error)
616
+ this.progress(0)
617
+ reject(res._error)
618
+ break
619
+ }
620
+ } else {
621
+ log('Response from worker:', res)
622
+ this.progress(0)
623
+ resolve(res)
534
624
  }
535
- } else {
536
- log('Response from worker:', res)
537
- resolve(res)
538
625
  }
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
- })
626
+ worker.onerror = (e) => {
627
+ notyf.error(e.message)
628
+ log('Error from worker:', e)
629
+ this.progress(0)
630
+ reject(e)
631
+ }
632
+ try {
633
+ worker.postMessage(payload)
634
+ } catch (error) {
635
+ const hasBinaryPayload = utils.containsBinaryPayload(payload)
636
+ if (hasBinaryPayload) {
637
+ const message = 'Worker postMessage failed for payload with File/Blob/binary data. JSON fallback would drop that data.'
638
+ log(message, error)
639
+ reject(new Error(message))
640
+ return
641
+ }
642
+ log('Worker postMessage failed, retrying with JSON fallback. Complex objects may lose metadata.', error)
643
+ try {
644
+ const fallbackPayload = JSON.parse(JSON.stringify(payload))
645
+ worker.postMessage(fallbackPayload)
646
+ } catch (fallbackError) {
647
+ reject(fallbackError)
648
+ }
649
+ }
650
+ })
547
651
 
548
- // Initial worker call with model definition
549
- await modelFunc(model)
652
+ // Skip timeout for init call (loading model can be slow); apply to execution calls
653
+ if (isInitCall) return workerPromise
654
+
655
+ const timeoutPromise = new Promise((_, reject) => {
656
+ setTimeout(() => {
657
+ worker.terminate()
658
+ reject(new Error(`Worker timed out after ${timeoutMs}ms`))
659
+ }, timeoutMs)
660
+ })
661
+ return Promise.race([workerPromise, timeoutPromise])
662
+ }
663
+
664
+ // Initial worker call with model definition and stream input config
665
+ const modelInitPayload = Object.assign({}, model, {
666
+ _streamInputConfig: this.streamInputConfig || {}
667
+ })
668
+ await modelFunc(modelInitPayload)
550
669
 
551
670
  // Worker will be in the context of each modelFunc
552
671
  return modelFunc
@@ -640,54 +759,85 @@ export default class JSEE {
640
759
  // 1. custom input button name
641
760
  // 2. `run`
642
761
  // 3. `autorun`
762
+
763
+ // Prevent overlapping runs: autorun skips, manual clicks queue
764
+ // Prevent overlapping runs: reactive/autorun calls are dropped, manual clicks queue
765
+ if (this.running) {
766
+ if (caller === 'autorun' || caller === 'reactive') return
767
+ log('Run already in progress, queuing', caller)
768
+ this._pendingRun = caller
769
+ return
770
+ }
771
+
643
772
  const schema = this.schema
644
773
  const data = this.data
645
774
  this.running = true
775
+ this.cancelled = false
776
+ // Run token to detect stale results when worker.onmessage gets rebound
777
+ const runToken = this._runToken = {}
646
778
 
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
779
+ try {
780
+ log('Running the pipeline...')
781
+ // Collect input values
782
+ let inputValues = {}
783
+ data.inputs.forEach(input => {
784
+ // Skip buttons
785
+ if (input.name && !(input.type == 'action' || input.type == 'button')) {
786
+ inputValues[input.name] = getValue(input)
787
+ }
788
+ })
789
+ // Add caller to input values so we can change model behavior based on it
790
+ inputValues.caller = caller
658
791
 
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) {
792
+ log('Input values:', inputValues)
662
793
  this.overlay.show()
663
- }
794
+ if (this.stopElement) {
795
+ this.stopElement.style.display = 'inline-block'
796
+ }
664
797
 
665
- // Run pipeline
666
- const results = await this.pipeline(inputValues)
798
+ // Run pipeline
799
+ const results = await this.pipeline(inputValues)
667
800
 
668
- // Output results
669
- this.output(results)
801
+ // Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
802
+ if (this._runToken !== runToken) return
670
803
 
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
- }
804
+ // Output results
805
+ this.output(results)
677
806
 
678
- // Hide overlay
679
- this.overlay.hide()
680
- return
807
+ // Check if interval is defined
808
+ if (utils.shouldContinueInterval(schema.interval, this.running, this.isCancelled(), caller)) {
809
+ log('Interval is defined:', schema.interval)
810
+ await utils.delay(schema.interval)
811
+ await this.run(caller)
812
+ }
813
+ } catch (err) {
814
+ // Surface pipeline/worker errors so they don't silently swallow failures
815
+ log('Pipeline error:', err)
816
+ notyf.error(typeof err === 'string' ? err : (err.message || 'Pipeline error'))
817
+ } finally {
818
+ // Always clean up UI state so overlay and running flag don't get stuck
819
+ this.overlay.hide()
820
+ if (this.stopElement) {
821
+ this.stopElement.style.display = 'none'
822
+ }
823
+ this.running = false
824
+
825
+ // Drain queued run if a manual click arrived while we were running
826
+ if (this._pendingRun) {
827
+ const pending = this._pendingRun
828
+ this._pendingRun = null
829
+ this.run(pending).catch(err => log('Queued run error:', err))
830
+ }
831
+ }
681
832
  }
682
833
 
683
834
  async outputAsync (res) {
684
835
  this.output(res)
685
- await delay(1)
836
+ await utils.delay(1)
686
837
  }
687
838
 
688
839
  output (res) {
689
- // TODO: Think about all edge cases
690
- // * No output field, but reactivity
840
+ // Edge case: no output field with reactivity is handled — undefined results exit early
691
841
 
692
842
  if (typeof res === 'undefined') {
693
843
  return
@@ -695,28 +845,65 @@ export default class JSEE {
695
845
 
696
846
  log('[Output] Got output results of type:', typeof res)
697
847
 
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
848
+ const inputNames = this.schema.inputs ? this.schema.inputs.map(i => i.name) : []
849
+ log('Input names:', inputNames)
850
+
851
+ if (isObject(res)) {
852
+ // Drop system fields
853
+ delete res.caller
854
+ delete res.stop
855
+ delete res._status
856
+ delete res._log
857
+ delete res._progress
858
+ log('Processing results as an object:', res)
859
+
860
+ if (Object.keys(res).every(key => inputNames.includes(key))) {
861
+ // Update input fields from results
862
+ // e.g. loading a csv file and updating list of target variables
863
+ // This will be dynamically updated in the UI
864
+ log('Updating inputs from results with keys:', Object.keys(res))
865
+ this.data.inputs.forEach((input, i) => {
866
+ if (input.name && (typeof res[input.name] !== 'undefined')) {
867
+ log(`Updating input: ${input.name} with data: ${res[input.name]}`)
868
+ const r = res[input.name]
869
+ if (typeof r === 'object') {
870
+ Object.keys(r).forEach(k => {
871
+ input[k] = r[k]
872
+ })
873
+ } else {
874
+ input.value = r
875
+ }
713
876
  }
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)
877
+ })
878
+ } else if (this.data.outputs && this.data.outputs.length) {
879
+ // Update outputs from results
880
+ log('Updating outputs from results with keys:', Object.keys(res))
881
+ this.data.outputs.forEach((output, i) => {
882
+ // try output.name, sanitized output.name, output.alias
883
+ const r = res[output.name]
884
+ || res[utils.sanitizeName(output.name)]
885
+ || (output.alias && res[output.alias])
886
+ if (typeof r !== 'undefined') {
887
+ log(`Updating output: ${output.name} with data: ${typeof r}`)
888
+ output.value = r
889
+ }
890
+ })
891
+ } else if (!this.schema.render && !this.schema.view) {
892
+ // There's no render or view defined in the schema, also:
893
+ // No outputs defined, create outputs from results
894
+ log('Creating outputs from results with keys:', Object.keys(res))
895
+ this.data.outputs = Object.keys(res)
896
+ .filter(key => !inputNames.includes(key))
897
+ .filter(key => key !== 'caller') // Filter out caller
898
+ .map(key => {
899
+ return {
900
+ 'name': key,
901
+ // typeof returns 'object' for arrays; distinguish them for proper rendering
902
+ 'type': Array.isArray(res[key]) ? 'array' : typeof res[key],
903
+ 'value': res[key]
904
+ }
905
+ })
906
+ }
720
907
  } else if (Array.isArray(res) && res.length) {
721
908
  // Result is array
722
909
  if (this.data.outputs && this.data.outputs.length) {
@@ -737,20 +924,6 @@ export default class JSEE {
737
924
  'value': res
738
925
  }]
739
926
  }
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
927
  } else if (this.schema.outputs && this.schema.outputs.length === 1) {
755
928
  // One output value passed as raw js object
756
929
  this.data.outputs[0].value = res
@@ -798,11 +971,11 @@ export default class JSEE {
798
971
  console.error('Error removing GA script tags:', error.message)
799
972
  }
800
973
 
801
- console.log('Caching schema:', env.schema)
802
- storeInHiddenElement(env.schemaUrl, env.schema)
974
+ console.log('Caching schema:', this.schema)
975
+ storeInHiddenElement(this.schemaUrl, this.schema)
803
976
 
804
- console.log('Caching models:', env.model)
805
- for (const model of env.model) {
977
+ console.log('Caching models:', this.model)
978
+ for (const model of this.model) {
806
979
  storeInHiddenElement(model.url, model.code)
807
980
  // Iterate over imports
808
981
  if (model.imports) {