@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/.claude/settings.local.json +9 -0
- package/AGENTS.md +37 -0
- package/CHANGELOG.md +71 -0
- package/CLAUDE.md +5 -0
- package/README.md +20 -41
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +2 -1
- package/jest.unit.config.js +8 -0
- package/load/index.html +9 -4
- package/package.json +15 -13
- package/src/app.js +35 -11
- package/src/cli.js +662 -548
- package/src/main.js +348 -152
- package/src/utils.js +590 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +22 -18
- package/templates/bulma-output.vue +72 -7
- package/templates/common-inputs.js +2 -13
- package/templates/common-outputs.js +57 -2
- package/templates/file-picker-base.vue +169 -0
- package/templates/file-picker.vue +318 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +286 -11
- package/test/unit/utils.test.js +519 -0
- package/webpack.config.js +1 -0
package/src/main.js
CHANGED
|
@@ -52,19 +52,7 @@ function clone (obj) {
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
|
|
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
|
-
|
|
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()
|
|
186
|
-
await this.initModel()
|
|
187
|
-
await this.initInputs()
|
|
188
|
-
|
|
189
|
-
await this.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.
|
|
423
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
820
|
+
// Run pipeline
|
|
821
|
+
const results = await this.pipeline(inputValues)
|
|
667
822
|
|
|
668
|
-
|
|
669
|
-
|
|
823
|
+
// Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
|
|
824
|
+
if (this._runToken !== runToken) return
|
|
670
825
|
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|