@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/.claude/settings.local.json +12 -0
- package/.eslintrc.js +38 -0
- package/AGENTS.md +38 -0
- package/CHANGELOG.md +86 -0
- package/CLAUDE.md +5 -0
- package/README.md +60 -42
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +7 -5
- package/jest.unit.config.js +8 -0
- package/load/index.html +16 -4
- package/package.json +17 -13
- package/src/app.js +35 -11
- package/src/cli.js +591 -330
- package/src/constants.js +12 -0
- package/src/main.js +356 -183
- package/src/utils.js +748 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +23 -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 +350 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +383 -17
- package/test/test-python.test.js +2 -5
- package/test/unit/cli-fetch.test.js +126 -0
- package/test/unit/utils.test.js +806 -0
- package/webpack.config.js +1 -0
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
|
-
|
|
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 (
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
152
|
-
this.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
|
-
|
|
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()
|
|
186
|
-
await this.initModel()
|
|
187
|
-
await this.initInputs()
|
|
188
|
-
|
|
189
|
-
await this.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.
|
|
423
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
-
|
|
798
|
+
// Run pipeline
|
|
799
|
+
const results = await this.pipeline(inputValues)
|
|
667
800
|
|
|
668
|
-
|
|
669
|
-
|
|
801
|
+
// Drop stale results if a newer run started (e.g. worker.onmessage rebound race)
|
|
802
|
+
if (this._runToken !== runToken) return
|
|
670
803
|
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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:',
|
|
802
|
-
storeInHiddenElement(
|
|
974
|
+
console.log('Caching schema:', this.schema)
|
|
975
|
+
storeInHiddenElement(this.schemaUrl, this.schema)
|
|
803
976
|
|
|
804
|
-
console.log('Caching models:',
|
|
805
|
-
for (const model of
|
|
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) {
|