@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/utils.js CHANGED
@@ -3,6 +3,421 @@ function isObject (item) {
3
3
  return (typeof item === 'object' && !Array.isArray(item) && item !== null)
4
4
  }
5
5
 
6
+ function shouldPreserveWorkerValue (value) {
7
+ if (!value || typeof value !== 'object') {
8
+ return true
9
+ }
10
+ if ((typeof File !== 'undefined') && (value instanceof File)) {
11
+ return true
12
+ }
13
+ if ((typeof Blob !== 'undefined') && (value instanceof Blob)) {
14
+ return true
15
+ }
16
+ if ((typeof ArrayBuffer !== 'undefined') && (value instanceof ArrayBuffer)) {
17
+ return true
18
+ }
19
+ if ((typeof ArrayBuffer !== 'undefined') && ArrayBuffer.isView && ArrayBuffer.isView(value)) {
20
+ return true
21
+ }
22
+ if ((typeof Date !== 'undefined') && (value instanceof Date)) {
23
+ return true
24
+ }
25
+ if ((typeof RegExp !== 'undefined') && (value instanceof RegExp)) {
26
+ return true
27
+ }
28
+ if ((typeof URL !== 'undefined') && (value instanceof URL)) {
29
+ return true
30
+ }
31
+ if ((typeof Map !== 'undefined') && (value instanceof Map)) {
32
+ return true
33
+ }
34
+ if ((typeof Set !== 'undefined') && (value instanceof Set)) {
35
+ return true
36
+ }
37
+ return false
38
+ }
39
+
40
+ const VALID_INPUT_TYPES = [
41
+ 'int',
42
+ 'float',
43
+ 'number',
44
+ 'string',
45
+ 'color',
46
+ 'text',
47
+ 'categorical',
48
+ 'select',
49
+ 'bool',
50
+ 'checkbox',
51
+ 'file',
52
+ 'group',
53
+ 'action',
54
+ 'button'
55
+ ]
56
+
57
+ const VALID_MODEL_TYPES = [
58
+ 'function',
59
+ 'class',
60
+ 'async-init',
61
+ 'async-function',
62
+ 'get',
63
+ 'post',
64
+ 'py',
65
+ 'tf'
66
+ ]
67
+
68
+ function sanitizeName (inputName) {
69
+ return inputName.toLowerCase().replace(/[^a-z0-9_]/g, '_')
70
+ }
71
+
72
+ function isWorkerInitMessage (data, initialized=false) {
73
+ if (initialized || !isObject(data)) {
74
+ return false
75
+ }
76
+ return (typeof data.url !== 'undefined') || (typeof data.code !== 'undefined')
77
+ }
78
+
79
+ function getProgressState (value) {
80
+ if (value === null) {
81
+ return {
82
+ mode: 'indeterminate',
83
+ value: null
84
+ }
85
+ }
86
+
87
+ const progressValue = Number(value)
88
+ if (Number.isNaN(progressValue)) {
89
+ return null
90
+ }
91
+
92
+ return {
93
+ mode: 'determinate',
94
+ value: Math.max(0, Math.min(100, progressValue))
95
+ }
96
+ }
97
+
98
+ function shouldContinueInterval (interval, running, cancelled, caller) {
99
+ return Boolean(interval) && running && !cancelled && caller === 'run'
100
+ }
101
+
102
+ function createAbortError (message='Operation aborted') {
103
+ const error = new Error(message)
104
+ error.name = 'AbortError'
105
+ return error
106
+ }
107
+
108
+ function isAbortRequested (signal, isCancelled) {
109
+ return !!(
110
+ (signal && signal.aborted)
111
+ || (typeof isCancelled === 'function' && isCancelled())
112
+ )
113
+ }
114
+
115
+ function isFileLikeSource (source) {
116
+ return !!source
117
+ && (typeof source === 'object')
118
+ && (typeof source.slice === 'function')
119
+ && (typeof source.size === 'number')
120
+ }
121
+
122
+ function getUrlFromSource (source) {
123
+ if (typeof source === 'string') {
124
+ return source
125
+ }
126
+ if (source && source.kind === 'url' && (typeof source.url === 'string')) {
127
+ return source.url
128
+ }
129
+ return null
130
+ }
131
+
132
+ // Async channel with backpressure for streaming chunks
133
+ function createChunkChannel () {
134
+ const queue = []
135
+ let waitingConsumer = null // resolve fn when consumer awaits data
136
+ let waitingProducer = null // resolve fn when producer awaits drain
137
+ let done = false
138
+ let error = null
139
+ const HIGH_WATER = 4
140
+
141
+ return {
142
+ async push (chunk) {
143
+ if (done || error) return
144
+ queue.push(chunk)
145
+ if (waitingConsumer) {
146
+ const resolve = waitingConsumer
147
+ waitingConsumer = null
148
+ resolve()
149
+ }
150
+ if (queue.length >= HIGH_WATER) {
151
+ await new Promise(resolve => { waitingProducer = resolve })
152
+ }
153
+ },
154
+ close () {
155
+ done = true
156
+ if (waitingConsumer) {
157
+ const resolve = waitingConsumer
158
+ waitingConsumer = null
159
+ resolve()
160
+ }
161
+ },
162
+ fail (err) {
163
+ error = err
164
+ done = true
165
+ if (waitingConsumer) {
166
+ const resolve = waitingConsumer
167
+ waitingConsumer = null
168
+ resolve()
169
+ }
170
+ },
171
+ [Symbol.asyncIterator] () {
172
+ return {
173
+ async next () {
174
+ while (queue.length === 0 && !done) {
175
+ await new Promise(resolve => { waitingConsumer = resolve })
176
+ }
177
+ if (queue.length > 0) {
178
+ const value = queue.shift()
179
+ if (waitingProducer && queue.length < HIGH_WATER) {
180
+ const resolve = waitingProducer
181
+ waitingProducer = null
182
+ resolve()
183
+ }
184
+ return { value, done: false }
185
+ }
186
+ if (error) {
187
+ throw error
188
+ }
189
+ return { value: undefined, done: true }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // Lightweight async-iterable reader for chunked data (File or fetch)
197
+ class ChunkedReader {
198
+ constructor (channel) {
199
+ this._channel = channel
200
+ }
201
+
202
+ [Symbol.asyncIterator] () {
203
+ return this._channel[Symbol.asyncIterator]()
204
+ }
205
+
206
+ async text () {
207
+ const decoder = new TextDecoder('utf-8')
208
+ let result = ''
209
+ for await (const chunk of this) {
210
+ result += decoder.decode(chunk, { stream: true })
211
+ }
212
+ result += decoder.decode()
213
+ return result
214
+ }
215
+
216
+ async bytes () {
217
+ const parts = []
218
+ let totalLength = 0
219
+ for await (const chunk of this) {
220
+ parts.push(chunk)
221
+ totalLength += chunk.byteLength
222
+ }
223
+ const result = new Uint8Array(totalLength)
224
+ let offset = 0
225
+ for (const part of parts) {
226
+ result.set(part, offset)
227
+ offset += part.byteLength
228
+ }
229
+ return result
230
+ }
231
+
232
+ async * lines () {
233
+ const decoder = new TextDecoder('utf-8')
234
+ let remainder = ''
235
+ for await (const chunk of this) {
236
+ remainder += decoder.decode(chunk, { stream: true })
237
+ const parts = remainder.split('\n')
238
+ remainder = parts.pop()
239
+ for (const line of parts) {
240
+ yield line
241
+ }
242
+ }
243
+ remainder += decoder.decode()
244
+ if (remainder.length > 0) {
245
+ yield remainder
246
+ }
247
+ }
248
+ }
249
+
250
+ function createChunkedReader (producer) {
251
+ const channel = createChunkChannel()
252
+
253
+ Promise.resolve()
254
+ .then(() => producer(
255
+ chunk => channel.push(chunk),
256
+ () => channel.close(),
257
+ err => channel.fail(err)
258
+ ))
259
+ .catch(err => channel.fail(err))
260
+
261
+ return new ChunkedReader(channel)
262
+ }
263
+
264
+ function createFileStream (source, options={}) {
265
+ const onProgress = options.onProgress
266
+ const signal = options.signal
267
+ const isCancelled = options.isCancelled
268
+ const chunkSize = (typeof options.chunkSize === 'number') && options.chunkSize > 0
269
+ ? Math.floor(options.chunkSize)
270
+ : (256 * 1024)
271
+
272
+ return createChunkedReader(async (pushChunk, closeStream, failStream) => {
273
+ const totalBytes = source.size
274
+ let loadedBytes = 0
275
+
276
+ const reportProgress = async (value) => {
277
+ if (typeof onProgress === 'function') {
278
+ await onProgress(value)
279
+ }
280
+ }
281
+
282
+ try {
283
+ await reportProgress(totalBytes > 0 ? 0 : null)
284
+ while (loadedBytes < totalBytes) {
285
+ if (isAbortRequested(signal, isCancelled)) {
286
+ throw createAbortError('createFileStream: aborted')
287
+ }
288
+ const nextOffset = Math.min(loadedBytes + chunkSize, totalBytes)
289
+ const blob = source.slice(loadedBytes, nextOffset)
290
+ const value = new Uint8Array(await blob.arrayBuffer())
291
+ loadedBytes = nextOffset
292
+ if (value.byteLength > 0) {
293
+ await pushChunk(value)
294
+ }
295
+ const progressValue = totalBytes > 0
296
+ ? Math.round((loadedBytes / totalBytes) * 100)
297
+ : null
298
+ await reportProgress(progressValue)
299
+ }
300
+ await reportProgress(totalBytes > 0 ? 100 : null)
301
+ closeStream()
302
+ } catch (error) {
303
+ failStream(error)
304
+ }
305
+ })
306
+ }
307
+
308
+ function createFetchStream (source, options={}) {
309
+ const sourceUrl = getUrlFromSource(source)
310
+ if (!sourceUrl) {
311
+ throw new Error('createFetchStream: unsupported source type')
312
+ }
313
+ const onProgress = options.onProgress
314
+ const signal = options.signal
315
+ const isCancelled = options.isCancelled
316
+ const fetchImpl = options.fetch || (typeof fetch === 'function' ? fetch : null)
317
+ if (!fetchImpl) {
318
+ throw new Error('createFetchStream: fetch is not available')
319
+ }
320
+
321
+ return createChunkedReader(async (pushChunk, closeStream, failStream) => {
322
+ const reportProgress = async (value) => {
323
+ if (typeof onProgress === 'function') {
324
+ await onProgress(value)
325
+ }
326
+ }
327
+
328
+ const abortController = typeof AbortController !== 'undefined'
329
+ ? new AbortController()
330
+ : null
331
+ const fetchSignal = abortController ? abortController.signal : signal
332
+ if (signal && abortController) {
333
+ if (signal.aborted) {
334
+ abortController.abort()
335
+ } else {
336
+ signal.addEventListener('abort', () => abortController.abort(), { once: true })
337
+ }
338
+ }
339
+
340
+ let reader
341
+ try {
342
+ if (isAbortRequested(signal, isCancelled)) {
343
+ throw createAbortError('createFetchStream: aborted before fetch')
344
+ }
345
+ const response = await fetchImpl(sourceUrl, fetchSignal ? { signal: fetchSignal } : {})
346
+ if (!response.ok) {
347
+ throw new Error(`createFetchStream: failed to fetch ${sourceUrl} (${response.status})`)
348
+ }
349
+ if (!response.body) {
350
+ throw new Error(`createFetchStream: empty response body for ${sourceUrl}`)
351
+ }
352
+
353
+ const totalBytesHeader = response.headers.get('content-length')
354
+ const totalBytes = totalBytesHeader ? Number(totalBytesHeader) : null
355
+ const hasKnownLength = !!totalBytes && !Number.isNaN(totalBytes) && totalBytes > 0
356
+ let loadedBytes = 0
357
+ reader = response.body.getReader()
358
+
359
+ await reportProgress(hasKnownLength ? 0 : null)
360
+ while (true) {
361
+ if (isAbortRequested(signal, isCancelled)) {
362
+ if (abortController) {
363
+ abortController.abort()
364
+ }
365
+ throw createAbortError('createFetchStream: aborted during read')
366
+ }
367
+ const { done, value } = await reader.read()
368
+ if (done) {
369
+ break
370
+ }
371
+ loadedBytes += value.byteLength
372
+ if (value.byteLength > 0) {
373
+ await pushChunk(value)
374
+ }
375
+ const progressValue = hasKnownLength
376
+ ? Math.round((loadedBytes / totalBytes) * 100)
377
+ : null
378
+ await reportProgress(progressValue)
379
+ }
380
+ await reportProgress(hasKnownLength ? 100 : null)
381
+ closeStream()
382
+ } catch (error) {
383
+ failStream(error)
384
+ } finally {
385
+ if (reader) {
386
+ reader.releaseLock()
387
+ }
388
+ }
389
+ })
390
+ }
391
+
392
+ function wrapStreamInputs (inputs, streamConfig={}, options={}) {
393
+ if (!isObject(inputs)) {
394
+ return inputs
395
+ }
396
+
397
+ const wrapped = Object.assign({}, inputs)
398
+ Object.keys(streamConfig).forEach((inputName) => {
399
+ const config = streamConfig[inputName]
400
+ if (!config || config.stream !== true) {
401
+ return
402
+ }
403
+ if (typeof wrapped[inputName] === 'undefined' || wrapped[inputName] === null) {
404
+ return
405
+ }
406
+
407
+ const source = wrapped[inputName]
408
+ if (isFileLikeSource(source)) {
409
+ wrapped[inputName] = createFileStream(source, options)
410
+ return
411
+ }
412
+
413
+ const sourceUrl = getUrlFromSource(source)
414
+ if (sourceUrl) {
415
+ wrapped[inputName] = createFetchStream(sourceUrl, options)
416
+ }
417
+ })
418
+ return wrapped
419
+ }
420
+
6
421
  async function getModelFuncJS (model, target, app) {
7
422
  let modelFunc
8
423
  switch (model.type) {
@@ -82,7 +497,7 @@ function importScriptAsync (imp, async=true) {
82
497
  console.log('Script loaded from cache:', ev.detail.url)
83
498
  resolve({ status: true })
84
499
  })
85
- document.body.appendChild(scriptElement);console.log('1')
500
+ document.body.appendChild(scriptElement)
86
501
  document.body.appendChild(eventElement)
87
502
  } else {
88
503
  // Create script element from import.url
@@ -95,7 +510,7 @@ function importScriptAsync (imp, async=true) {
95
510
  scriptElement.addEventListener('error', (ev) => {
96
511
  reject({
97
512
  status: false,
98
- message: `Failed to import {imp.url}`
513
+ message: `Failed to import ${imp.url}`
99
514
  })
100
515
  })
101
516
  document.body.appendChild(scriptElement);
@@ -146,6 +561,167 @@ async function delay (ms) {
146
561
  return new Promise(resolve => setTimeout(resolve, ms || 1))
147
562
  }
148
563
 
564
+ function toWorkerSerializable (value) {
565
+ if (shouldPreserveWorkerValue(value)) {
566
+ return value
567
+ }
568
+
569
+ if (Array.isArray(value)) {
570
+ return value.map(item => toWorkerSerializable(item))
571
+ }
572
+
573
+ if (!isObject(value)) {
574
+ return value
575
+ }
576
+
577
+ const copy = {}
578
+ Object.keys(value).forEach(key => {
579
+ copy[key] = toWorkerSerializable(value[key])
580
+ })
581
+ return copy
582
+ }
583
+
584
+ // Simple debounce to prevent rapid-fire calls (e.g. autorun on every keystroke)
585
+ function debounce (fn, ms) {
586
+ let timer
587
+ return function (...args) {
588
+ clearTimeout(timer)
589
+ timer = setTimeout(() => fn.apply(this, args), ms)
590
+ }
591
+ }
592
+
593
+ // Extract function name from code (string or function reference).
594
+ // Handles 'function name', 'async function name'. Arrow/anonymous return undefined.
595
+ function getName (code) {
596
+ switch (typeof code) {
597
+ case 'function':
598
+ return code.name
599
+ case 'string':
600
+ const words = code.split(' ')
601
+ const functionIndex = words.findIndex((word) => word === 'function')
602
+ if (functionIndex === -1) return undefined
603
+ const name = words[functionIndex + 1]
604
+ if (!name || name.includes('(')) return undefined
605
+ return name
606
+ default:
607
+ return undefined
608
+ }
609
+ }
610
+
611
+ function validateInputSchema (input, path, report) {
612
+ if (!isObject(input)) {
613
+ report.errors.push(`${path} should be an object`)
614
+ return
615
+ }
616
+
617
+ if ((typeof input.name !== 'undefined') && (typeof input.name !== 'string')) {
618
+ report.warnings.push(`${path}.name should be a string`)
619
+ }
620
+
621
+ if ((typeof input.type !== 'undefined') && !VALID_INPUT_TYPES.includes(input.type)) {
622
+ report.warnings.push(`${path}.type '${input.type}' is not recognized`)
623
+ }
624
+
625
+ if ((typeof input.raw !== 'undefined') && (typeof input.raw !== 'boolean')) {
626
+ report.warnings.push(`${path}.raw should be a boolean`)
627
+ }
628
+
629
+ if ((typeof input.stream !== 'undefined') && (typeof input.stream !== 'boolean')) {
630
+ report.warnings.push(`${path}.stream should be a boolean`)
631
+ }
632
+ if ((input.stream === true) && (input.type !== 'file')) {
633
+ report.warnings.push(`${path}.stream is supported only for file inputs`)
634
+ }
635
+
636
+ if (input.type === 'group') {
637
+ if (!Array.isArray(input.elements)) {
638
+ report.warnings.push(`${path}.elements should be an array for group inputs`)
639
+ } else {
640
+ input.elements.forEach((element, index) => {
641
+ validateInputSchema(element, `${path}.elements[${index}]`, report)
642
+ })
643
+ }
644
+ }
645
+
646
+ if (typeof input.alias !== 'undefined') {
647
+ const validAlias = (
648
+ typeof input.alias === 'string'
649
+ || (Array.isArray(input.alias) && input.alias.every(alias => typeof alias === 'string'))
650
+ )
651
+ if (!validAlias) {
652
+ report.warnings.push(`${path}.alias should be a string or an array of strings`)
653
+ }
654
+ }
655
+ }
656
+
657
+ function validateModelSchema (model, path, report) {
658
+ if (typeof model === 'function') {
659
+ return
660
+ }
661
+
662
+ if (!isObject(model)) {
663
+ report.errors.push(`${path} should be an object or function`)
664
+ return
665
+ }
666
+
667
+ if ((typeof model.type !== 'undefined') && !VALID_MODEL_TYPES.includes(model.type)) {
668
+ report.warnings.push(`${path}.type '${model.type}' is not recognized`)
669
+ }
670
+
671
+ if ((typeof model.worker !== 'undefined') && (typeof model.worker !== 'boolean')) {
672
+ report.warnings.push(`${path}.worker should be a boolean`)
673
+ }
674
+
675
+ if (typeof model.timeout !== 'undefined') {
676
+ const timeoutValid = (typeof model.timeout === 'number') && !Number.isNaN(model.timeout) && (model.timeout > 0)
677
+ if (!timeoutValid) {
678
+ report.warnings.push(`${path}.timeout should be a positive number`)
679
+ }
680
+ }
681
+ }
682
+
683
+ function validateSchema (schema) {
684
+ const report = {
685
+ errors: [],
686
+ warnings: []
687
+ }
688
+
689
+ if (!isObject(schema)) {
690
+ report.errors.push('Schema should be an object')
691
+ return report
692
+ }
693
+
694
+ const hasModel = typeof schema.model !== 'undefined'
695
+ const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
696
+
697
+ if (!hasModel && !hasView) {
698
+ report.errors.push('Schema should define `model` (or `view`/`render`)')
699
+ } else if (!hasModel && hasView) {
700
+ report.warnings.push('Schema has no `model`, using `view`/`render` only')
701
+ }
702
+
703
+ if (typeof schema.inputs !== 'undefined') {
704
+ if (!Array.isArray(schema.inputs)) {
705
+ report.errors.push('`inputs` should be an array')
706
+ } else {
707
+ schema.inputs.forEach((input, index) => {
708
+ validateInputSchema(input, `inputs[${index}]`, report)
709
+ })
710
+ }
711
+ }
712
+
713
+ if (hasModel) {
714
+ const models = Array.isArray(schema.model)
715
+ ? schema.model
716
+ : [schema.model]
717
+ models.forEach((model, index) => {
718
+ validateModelSchema(model, `model[${index}]`, report)
719
+ })
720
+ }
721
+
722
+ return report
723
+ }
724
+
149
725
  module.exports = {
150
726
  isObject,
151
727
  loadFromDOM,
@@ -153,5 +729,16 @@ module.exports = {
153
729
  getModelFuncAPI,
154
730
  importScripts,
155
731
  getUrl,
156
- delay
732
+ delay,
733
+ debounce,
734
+ sanitizeName,
735
+ isWorkerInitMessage,
736
+ getProgressState,
737
+ shouldContinueInterval,
738
+ createFileStream,
739
+ createFetchStream,
740
+ wrapStreamInputs,
741
+ getName,
742
+ validateSchema,
743
+ toWorkerSerializable
157
744
  }