@nxtedition/lib 26.8.8 → 27.0.0

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.
@@ -1,692 +0,0 @@
1
- import { makeWeakCache } from '../../weakCache.js'
2
- import * as rxjs from 'rxjs'
3
- import objectHash from 'object-hash'
4
- import * as datefns from 'date-fns'
5
- import JSON5 from 'json5'
6
- import fp from 'lodash/fp.js'
7
- import moment from 'moment-timezone'
8
- import Timecode from 'smpte-timecode'
9
- import transform from './transform.js'
10
- import { doYield } from '../../yield.js'
11
-
12
- const SEP = String.fromCharCode(30) // ASCII 1E
13
-
14
- const kSuspend = Symbol('kSuspend')
15
- const kEmpty = Symbol('kEmpty')
16
- const maxInt = 2147483647
17
-
18
- class TimerEntry {
19
- constructor(key, refresh, delay) {
20
- this.key = key
21
- this.counter = -1
22
-
23
- this.timer = setTimeout(refresh, delay)
24
- }
25
-
26
- dispose() {
27
- clearTimeout(this.timer)
28
- this.timer = null
29
- }
30
- }
31
-
32
- class FetchEntry {
33
- constructor(key, refresh, { resource, options, fetch }) {
34
- this.key = key
35
- this.counter = -1
36
- this.ac = new AbortController()
37
-
38
- this.refresh = refresh
39
- this.body = null
40
- this.status = null
41
- this.error = null
42
-
43
- let { headers, body, method } = options ?? {}
44
-
45
- if (fp.isPlainObject(body) || fp.isArray(body)) {
46
- headers = { 'content-type': 'application/json', ...headers }
47
- body = JSON.stringify(body)
48
- }
49
-
50
- if (!method && body) {
51
- method = 'POST'
52
- }
53
-
54
- try {
55
- fetch(resource, {
56
- ...options,
57
- headers,
58
- body,
59
- signal: this.ac.signal,
60
- })
61
- .then(async (res) => {
62
- // TODO (fix): Check cache-control and invalidate entry...
63
- const body = await res.body.text()
64
- if (this.refresh) {
65
- // TODO (fix): max size...
66
- this.status = res.statusCode
67
- this.headers = res.headers
68
- this.body = body
69
- this.refresh()
70
- }
71
- })
72
- .catch((err) => {
73
- if (this.refresh) {
74
- this.error = Object.assign(new Error('fetch failed'), {
75
- data: { resource },
76
- cause: err,
77
- })
78
- this.refresh()
79
- }
80
- })
81
- } catch (err) {
82
- this.error = Object.assign(new Error('fetch failed'), { data: { resource }, cause: err })
83
- this.refresh()
84
- }
85
- }
86
-
87
- dispose() {
88
- this.refresh = null
89
-
90
- this.ac.abort()
91
- this.ac = null
92
- }
93
- }
94
-
95
- class RecordEntry {
96
- constructor(key, refresh, { name, path, ds, state }) {
97
- if (state == null) {
98
- state =
99
- name.startsWith('{') || name.includes('?')
100
- ? ds.record.STATE.PROVIDER
101
- : ds.record.STATE.SERVER
102
- } else if (typeof state === 'string') {
103
- state = ds.record.STATE[state.toUpperCase()] ?? ds.record.STATE.SERVER
104
- }
105
-
106
- this.key = key
107
- this.counter = -1
108
-
109
- this.record = ds.record.getRecord(name)
110
-
111
- this.data = this.record.get(path)
112
- this.ready = this.record.state >= state
113
- this.refresh = () => {
114
- const data = this.record.get(path)
115
- const ready = this.record.state >= state
116
-
117
- if (data !== this.data || ready !== this.ready) {
118
- this.data = data
119
- this.ready = ready
120
- refresh()
121
- }
122
- }
123
-
124
- if (this.record.subscribe) {
125
- this.record.subscribe(this.refresh)
126
- } else {
127
- this.record.on('update', this.refresh)
128
- }
129
- }
130
-
131
- dispose() {
132
- this.record.unref()
133
- if (this.record.unsubscribe) {
134
- this.record.unsubscribe(this.refresh)
135
- } else {
136
- this.record.off('update', this.refresh)
137
- }
138
-
139
- this.record = null
140
- this.refresh = null
141
- this.data = null
142
- this.ready = false
143
- }
144
- }
145
-
146
- class ObservableEntry {
147
- constructor(key, refresh, observable) {
148
- this.key = key
149
- this.counter = -1
150
- this.value = kEmpty
151
- this.error = null
152
- this.refresh = refresh
153
- this.sync = true
154
- this.subscription = observable.subscribe({
155
- next: (value) => {
156
- this.value = value
157
- if (!this.sync) {
158
- this.refresh()
159
- }
160
- },
161
- error: (err) => {
162
- this.error = err
163
- if (!this.sync) {
164
- this.refresh()
165
- }
166
- },
167
- })
168
- this.sync = false
169
- }
170
-
171
- dispose() {
172
- this.subscription.unsubscribe()
173
- this.subscription = null
174
- this.refresh = null
175
- }
176
- }
177
-
178
- class PromiseEntry {
179
- constructor(key, refresh, promise) {
180
- this.key = key
181
- this.counter = -1
182
- this.value = kEmpty
183
- this.error = null
184
- this.refresh = refresh
185
-
186
- promise.then(
187
- (value) => {
188
- if (this.refresh) {
189
- this.value = value
190
- this.refresh()
191
- }
192
- },
193
- (err) => {
194
- if (this.refresh) {
195
- this.error = err
196
- this.refresh()
197
- }
198
- },
199
- )
200
- }
201
-
202
- dispose() {
203
- this.refresh = null
204
- }
205
- }
206
-
207
- function proxify(value, expression, handler, suspend = true) {
208
- if (!expression) {
209
- throw new Error('expression is not defined')
210
- }
211
- if (!handler) {
212
- throw new Error('handler is not defined')
213
- }
214
-
215
- if (!value) {
216
- return value
217
- } else if (rxjs.isObservable(value)) {
218
- return proxify(expression.observe(value, suspend), expression, handler, suspend)
219
- } else if (typeof value?.then === 'function') {
220
- return proxify(expression.wait(value, suspend), expression, handler, suspend)
221
- } else if (typeof value === 'object') {
222
- return new Proxy(value, handler)
223
- } else {
224
- return value
225
- }
226
- }
227
-
228
- function makeWrapper(expression) {
229
- const handler = {
230
- get: (target, prop) => proxify(target[prop], expression, handler),
231
- }
232
- return (value, suspend = true) => proxify(value, expression, handler, suspend)
233
- }
234
-
235
- export default function ({ ds, proxify, compiler, logger, platform }) {
236
- const schedule = platform?.schedule ?? doYield
237
-
238
- class Expression {
239
- constructor(script, expression, args, observer) {
240
- this._expression = expression
241
- this._observer = observer
242
- this._script = script
243
-
244
- // TODO (perf): This could be faster by using an array + indices.
245
- // A bit similar to how react-hooks works.
246
- this._entries = new Map()
247
- this._refreshing = false
248
- this._counter = 0
249
- this._value = kEmpty
250
- this._disposing = false
251
- this._destroyed = false
252
- this._subscription = null
253
- this._args = kEmpty
254
- this._wrap = null
255
- this._suspended = false
256
- this._errored = false
257
- this._globalThis = {
258
- fp: null,
259
- moment: null,
260
- Timecode: null,
261
- datefns: null,
262
- JSON5: null,
263
- $: null,
264
- nxt: null,
265
- }
266
-
267
- if (rxjs.isObservable(args)) {
268
- this._subscription = args.subscribe({
269
- next: (args) => {
270
- this._args = proxify ? this.wrap(args) : args
271
- this._refresh()
272
- },
273
- error: (err) => {
274
- this._observer.error(err)
275
- },
276
- })
277
- } else {
278
- this._args = proxify ? this.wrap(args) : args
279
- this._refresh()
280
- }
281
- }
282
-
283
- wrap(value, suspend = true) {
284
- this._wrap ??= makeWrapper(this)
285
- return this._wrap(value, suspend)
286
- }
287
-
288
- suspend() {
289
- throw kSuspend
290
- }
291
-
292
- fetch(resource, options, suspend) {
293
- return this._getFetch(resource, options, suspend)
294
- }
295
-
296
- observe(observable, suspend) {
297
- return this._getObservable(observable, suspend)
298
- }
299
-
300
- then(promise, suspend) {
301
- return this._getPromise(promise, suspend)
302
- }
303
-
304
- /** @deprecated */
305
- ds(name, state, suspend) {
306
- return !name || typeof name !== 'string'
307
- ? ds.record.JSON.EMPTY_OBJ
308
- : this._getRecord(name, null, state, suspend)
309
- }
310
-
311
- get(name, path, state, suspend) {
312
- return !name || typeof name !== 'string'
313
- ? path
314
- ? undefined
315
- : ds.record.JSON.EMPTY_OBJ
316
- : this._getRecord(name, path, state, suspend)
317
- }
318
-
319
- timer(dueTime, dueValue = dueTime, suspend) {
320
- return this._getTimer(dueTime, dueValue, suspend)
321
- }
322
-
323
- asset(id, type, state, suspend) {
324
- return this._getHasRawAssetType(id, type, state, suspend)
325
- }
326
-
327
- hash(value) {
328
- return objectHash(value)
329
- }
330
-
331
- /** @deprecated */
332
- _ds(name, postfix, state, suspend) {
333
- return !name || typeof name !== 'string'
334
- ? ds.record.JSON.EMPTY_OBJ
335
- : this._getRecord(postfix ? name + postfix : name, null, state, suspend)
336
- }
337
-
338
- _get(name, postfix, path, state, suspend) {
339
- return !name || typeof name !== 'string'
340
- ? path
341
- ? undefined
342
- : ds.record.JSON.EMPTY_OBJ
343
- : this._getRecord(postfix ? name + postfix : name, path, state, suspend)
344
- }
345
-
346
- _asset(id, type, state, suspend) {
347
- if (!type || typeof type !== 'string') {
348
- throw new Error(`invalid argument: type (${type})`)
349
- }
350
-
351
- return !id || typeof id !== 'string'
352
- ? null
353
- : this._getHasRawAssetType(id, type, state, suspend)
354
- }
355
-
356
- _destroy() {
357
- this._destroyed = true
358
- this._subscription?.unsubscribe()
359
-
360
- for (const entry of this._entries.values()) {
361
- entry.dispose()
362
- }
363
- this._entries.clear()
364
- }
365
-
366
- _refreshImpl = () => {
367
- this._refreshing = false
368
-
369
- if (this._destroyed || this._disposing || this._args === kEmpty) {
370
- return
371
- }
372
-
373
- this._counter = (this._counter + 1) & maxInt
374
-
375
- const previous = compiler.current
376
- compiler.current = this
377
-
378
- try {
379
- if (this._suspended !== false) {
380
- throw new Error('expression is already suspended')
381
- }
382
-
383
- this._globalThis.fp = globalThis.fp
384
- this._globalThis.moment = globalThis.moment
385
- this._globalThis.Timecode = globalThis.Timecode
386
- this._globalThis.datefns = globalThis.datefns
387
- this._globalThis.JSON5 = globalThis.JSON5
388
- this._globalThis.$ = globalThis.$
389
- this._globalThis.nxt = globalThis.nxt
390
-
391
- let value
392
- try {
393
- globalThis.fp = fp
394
- globalThis.moment = moment
395
- globalThis.Timecode = Timecode
396
- globalThis.datefns = datefns
397
- globalThis.JSON5 = JSON5
398
- globalThis.$ = this._args
399
- globalThis.nxt = this
400
-
401
- // TODO (fix): This should be sandboxed somehow...
402
- // - Use a webworker?
403
- // - Use vm for Node only?
404
- // - Use https://github.com/tc39/proposal-shadowrealm?
405
- value = this._script()
406
- } finally {
407
- globalThis.fp = this._globalThis.fp
408
- globalThis.moment = this._globalThis.moment
409
- globalThis.Timecode = this._globalThis.Timecode
410
- globalThis.datefns = this._globalThis.datefns
411
- globalThis.JSON5 = this._globalThis.JSON5
412
- globalThis.$ = this._globalThis.$
413
- globalThis.nxt = this._globalThis.nxt
414
- }
415
-
416
- this._errored = false
417
- if (this._suspended) {
418
- return
419
- }
420
-
421
- if (value !== this._value) {
422
- this._value = value
423
- this._observer.next(value)
424
- }
425
- } catch (err) {
426
- if (err === kSuspend) {
427
- return
428
- }
429
-
430
- this._errored = true
431
-
432
- const error = Object.assign(new Error('expression failed'), {
433
- cause: err,
434
- data: { expression: this._expression, data: this._args },
435
- })
436
-
437
- if (this._suspended) {
438
- if (logger) {
439
- logger.error({ err: error })
440
- } else {
441
- process.emitWarning(error)
442
- }
443
- } else {
444
- this._observer.error(error)
445
- }
446
- } finally {
447
- compiler.current = previous
448
-
449
- this._suspended = false
450
- this._disposing = true
451
-
452
- for (const entry of this._entries.values()) {
453
- if (entry.counter !== this._counter) {
454
- entry.dispose()
455
- this._entries.delete(entry.key)
456
- }
457
- }
458
-
459
- // TODO (perf): Make this work.
460
- // if (!this._entries) {
461
- // this._args = null
462
- // }
463
-
464
- this._disposing = false
465
- }
466
- }
467
-
468
- _refresh = () => {
469
- if (this._refreshing || this._destroyed || this._disposing || this._args === kEmpty) {
470
- return
471
- }
472
-
473
- this._refreshing = true
474
- schedule(this._refreshImpl)
475
- }
476
-
477
- _getEntry(key, Entry, opaque) {
478
- let entry = this._entries.get(key)
479
- if (!entry) {
480
- entry = new Entry(key, this._refresh, opaque)
481
- this._entries.set(key, entry)
482
- }
483
- entry.counter = this._counter
484
- return entry
485
- }
486
-
487
- _getFetch(resource, options, suspend) {
488
- const key = JSON.stringify({ resource, options })
489
- const entry = this._getEntry(key, FetchEntry, { resource, options, fetch: platform.fetch })
490
-
491
- if (entry.refresh === null) {
492
- return null
493
- }
494
-
495
- if (entry.error) {
496
- throw entry.error
497
- }
498
-
499
- if (!entry.status) {
500
- this._suspended = true
501
- if (suspend ?? this._errored) {
502
- throw kSuspend
503
- } else {
504
- return null
505
- }
506
- }
507
-
508
- return { status: entry.status, headers: entry.headers, body: entry.body }
509
- }
510
-
511
- _getObservable(observable, suspend) {
512
- if (!rxjs.isObservable(observable)) {
513
- throw new Error(`invalid argument: observable (${observable})`)
514
- }
515
-
516
- const entry = this._getEntry(observable, ObservableEntry, observable)
517
-
518
- if (entry.refresh === null) {
519
- return null
520
- }
521
-
522
- if (entry.error) {
523
- throw entry.error
524
- }
525
-
526
- if (entry.value === kEmpty) {
527
- this._suspended = true
528
- if (suspend ?? this._errored) {
529
- throw kSuspend
530
- } else {
531
- return null
532
- }
533
- }
534
-
535
- return entry.value
536
- }
537
-
538
- _getPromise(promise, suspend) {
539
- if (typeof promise?.then !== 'function') {
540
- throw new Error(`invalid argument: Promise (${promise})`)
541
- }
542
-
543
- const entry = this._getEntry(promise, PromiseEntry, promise)
544
-
545
- if (entry.refresh === null) {
546
- return null
547
- }
548
-
549
- if (entry.error) {
550
- throw entry.error
551
- }
552
-
553
- if (entry.value === kEmpty) {
554
- this._suspended = true
555
- if (suspend ?? this._errored) {
556
- throw kSuspend
557
- } else {
558
- return null
559
- }
560
- }
561
-
562
- return entry.value
563
- }
564
-
565
- _getTimer(dueTime, dueValue = dueTime, suspend) {
566
- const key = JSON.stringify({ dueTime, dueValue })
567
-
568
- dueTime = Number.isFinite(dueTime) ? dueTime : new Date(dueTime).valueOf()
569
-
570
- const delay = dueTime - Date.now()
571
-
572
- if (Number.isFinite(dueTime) && delay > 0 && delay < 2 ** 31 - 1) {
573
- this._getEntry(key, TimerEntry, delay)
574
- if (suspend ?? this._errored) {
575
- throw kSuspend
576
- } else {
577
- return null
578
- }
579
- }
580
-
581
- return dueValue
582
- }
583
-
584
- _getRecord(name, path, state, suspend) {
585
- if (name != null && typeof name !== 'string') {
586
- throw new Error(`invalid argument: key (${name})`)
587
- }
588
-
589
- if (path != null && typeof path !== 'string') {
590
- throw new Error(`invalid argument: path (${path})`)
591
- }
592
-
593
- if (state != null && typeof state !== 'string' && typeof state !== 'number') {
594
- throw new Error(`invalid argument: state (${state})`)
595
- }
596
-
597
- if (typeof state === 'string') {
598
- state = ds.record.STATE[state.toUpperCase()]
599
- } else if (state == null) {
600
- state = ds.record.STATE.SERVER
601
- }
602
-
603
- const key = '' + (name ?? '') + SEP + (path ?? '') + SEP + (state ?? '')
604
- const entry = this._getEntry(key, RecordEntry, { ds, name, path, state })
605
-
606
- if (!entry.ready) {
607
- this._suspended = true
608
- if (suspend ?? this._errored) {
609
- throw kSuspend
610
- } else {
611
- return entry.data
612
- }
613
- }
614
-
615
- return entry.data
616
- }
617
-
618
- _getHasRawAssetType(id, type, state, suspend) {
619
- if (!id || typeof id !== 'string') {
620
- throw new Error(`invalid argument: id (${id})`)
621
- }
622
-
623
- if (!type || typeof type !== 'string') {
624
- throw new Error(`invalid argument: type (${type})`)
625
- }
626
-
627
- const rawTypes = this._getRecord(
628
- id + ':asset.rawTypes?',
629
- 'value',
630
- state ?? ds.record.PROVIDER,
631
- suspend,
632
- )
633
- return Array.isArray(rawTypes) && rawTypes.includes(type) ? id : null
634
- }
635
- }
636
-
637
- return makeWeakCache((expression) => {
638
- let script
639
- try {
640
- script = new Function(
641
- transform(`
642
- const _ = (value, ...fns) => {
643
- if (value == null) {
644
- return value
645
- }
646
-
647
- for (const fn of fns) {
648
- value = fn(value)
649
- if (value == null) {
650
- return value
651
- }
652
- }
653
-
654
- return value
655
- };
656
-
657
- const fp = globalThis.fp
658
- const moment = globalThis.moment
659
- const Timecode = globalThis.Timecode
660
- const datefns = globalThis.datefns
661
- const JSON5 = globalThis.JSON5
662
- const $ = globalThis.$
663
- const nxt = globalThis.nxt
664
-
665
- _.asset = (type, state, suspend) => (name) => nxt._asset(name, type, state, suspend);
666
- _.ds = (postfix, state, suspend) => (name) => nxt._ds(name, postfix, state, suspend);
667
- _.get = (postfix, path, state, suspend) => (name) => nxt._get(name, postfix, path, state, suspend);
668
- _.timer = (dueTime) => (dueValue) => nxt.timer(dueTime, dueValue);
669
- _.fetch = (options, suspend) => (resource) => nxt.fetch(resource, options, suspend);
670
- ${expression}
671
- `),
672
- )
673
- } catch (err) {
674
- return () =>
675
- rxjs.throwError(
676
- Object.assign(new Error(`failed to parse expression ${expression}`), { cause: err }),
677
- )
678
- }
679
-
680
- return (args) =>
681
- new rxjs.Observable((o) => {
682
- try {
683
- const exp = new Expression(script, expression, args, o)
684
- return () => {
685
- exp._destroy()
686
- }
687
- } catch (err) {
688
- o.error(err)
689
- }
690
- })
691
- })
692
- }