@naturalcycles/js-lib 14.256.0 → 14.258.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.
Files changed (76) hide show
  1. package/cfg/frontend/tsconfig.json +67 -0
  2. package/dist/browser/adminService.d.ts +69 -0
  3. package/dist/browser/adminService.js +98 -0
  4. package/dist/browser/analytics.util.d.ts +12 -0
  5. package/dist/browser/analytics.util.js +59 -0
  6. package/dist/browser/i18n/fetchTranslationLoader.d.ts +13 -0
  7. package/dist/browser/i18n/fetchTranslationLoader.js +17 -0
  8. package/dist/browser/i18n/translation.service.d.ts +53 -0
  9. package/dist/browser/i18n/translation.service.js +61 -0
  10. package/dist/browser/imageFitter.d.ts +60 -0
  11. package/dist/browser/imageFitter.js +69 -0
  12. package/dist/browser/script.util.d.ts +14 -0
  13. package/dist/browser/script.util.js +50 -0
  14. package/dist/browser/topbar.d.ts +23 -0
  15. package/dist/browser/topbar.js +137 -0
  16. package/dist/decorators/memo.util.d.ts +2 -1
  17. package/dist/decorators/memo.util.js +8 -6
  18. package/dist/decorators/swarmSafe.decorator.d.ts +9 -0
  19. package/dist/decorators/swarmSafe.decorator.js +42 -0
  20. package/dist/deviceIdService.d.ts +65 -0
  21. package/dist/deviceIdService.js +109 -0
  22. package/dist/error/assert.d.ts +2 -1
  23. package/dist/error/assert.js +15 -13
  24. package/dist/error/error.util.js +9 -6
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.js +9 -0
  27. package/dist/nanoid.d.ts +7 -0
  28. package/dist/nanoid.js +61 -0
  29. package/dist/number/createDeterministicRandom.d.ts +6 -1
  30. package/dist/number/createDeterministicRandom.js +1 -2
  31. package/dist/string/hash.util.d.ts +1 -1
  32. package/dist/string/hash.util.js +1 -1
  33. package/dist/web.d.ts +6 -0
  34. package/dist/web.js +6 -0
  35. package/dist/zod/zod.util.d.ts +1 -1
  36. package/dist-esm/browser/adminService.js +94 -0
  37. package/dist-esm/browser/analytics.util.js +54 -0
  38. package/dist-esm/browser/i18n/fetchTranslationLoader.js +13 -0
  39. package/dist-esm/browser/i18n/translation.service.js +56 -0
  40. package/dist-esm/browser/imageFitter.js +65 -0
  41. package/dist-esm/browser/script.util.js +46 -0
  42. package/dist-esm/browser/topbar.js +134 -0
  43. package/dist-esm/decorators/memo.util.js +3 -1
  44. package/dist-esm/decorators/swarmSafe.decorator.js +38 -0
  45. package/dist-esm/deviceIdService.js +105 -0
  46. package/dist-esm/error/assert.js +3 -1
  47. package/dist-esm/error/error.util.js +4 -1
  48. package/dist-esm/index.js +9 -0
  49. package/dist-esm/nanoid.js +57 -0
  50. package/dist-esm/number/createDeterministicRandom.js +1 -2
  51. package/dist-esm/string/hash.util.js +1 -1
  52. package/dist-esm/web.js +6 -0
  53. package/package.json +2 -1
  54. package/src/browser/adminService.ts +157 -0
  55. package/src/browser/analytics.util.ts +68 -0
  56. package/src/browser/i18n/fetchTranslationLoader.ts +16 -0
  57. package/src/browser/i18n/translation.service.ts +102 -0
  58. package/src/browser/imageFitter.ts +128 -0
  59. package/src/browser/script.util.ts +52 -0
  60. package/src/browser/topbar.ts +147 -0
  61. package/src/datetime/localDate.ts +16 -0
  62. package/src/datetime/localTime.ts +39 -0
  63. package/src/decorators/debounce.ts +1 -0
  64. package/src/decorators/memo.util.ts +4 -1
  65. package/src/decorators/swarmSafe.decorator.ts +47 -0
  66. package/src/deviceIdService.ts +137 -0
  67. package/src/error/assert.ts +5 -11
  68. package/src/error/error.util.ts +4 -1
  69. package/src/index.ts +9 -0
  70. package/src/json-schema/jsonSchemaBuilder.ts +20 -0
  71. package/src/nanoid.ts +79 -0
  72. package/src/number/createDeterministicRandom.ts +7 -2
  73. package/src/semver.ts +2 -0
  74. package/src/string/hash.util.ts +1 -1
  75. package/src/web.ts +6 -0
  76. package/src/zod/zod.util.ts +1 -1
@@ -0,0 +1,128 @@
1
+ export interface FitImagesCfg {
2
+ /**
3
+ * Container of the images
4
+ */
5
+ containerElement: HTMLElement
6
+
7
+ /**
8
+ * Array of image metadatas (most notably: aspectRatio).
9
+ */
10
+ images: FitImage[]
11
+
12
+ /**
13
+ * Will be called on each layout change.
14
+ * Should be listened to to update the width/height of the images in your DOM.
15
+ */
16
+ onChange: (images: FitImage[]) => any
17
+
18
+ /**
19
+ * Max image height in pixels.
20
+ *
21
+ * @default 300
22
+ */
23
+ maxHeight?: number
24
+
25
+ /**
26
+ * Margin between images.
27
+ *
28
+ * @default 8
29
+ */
30
+ margin?: number
31
+ }
32
+
33
+ export interface FitImage {
34
+ src: string
35
+
36
+ /**
37
+ * width divided by height
38
+ */
39
+ aspectRatio: number
40
+
41
+ /**
42
+ * Calculated image width to fit the layout.
43
+ */
44
+ fitWidth?: number
45
+ /**
46
+ * Calculated image height to fit the layout.
47
+ */
48
+ fitHeight?: number
49
+ }
50
+
51
+ /**
52
+ * Calculates the width/height of the images to fit in the layout.
53
+ *
54
+ * Currently does not mutate the cfg.images array, but DOES mutate individual images with .fitWidth, .fitHeight properties.
55
+ *
56
+ * @experimental
57
+ */
58
+ export class ImageFitter {
59
+ constructor(cfg: FitImagesCfg) {
60
+ this.cfg = {
61
+ maxHeight: 300,
62
+ margin: 8,
63
+ ...cfg,
64
+ }
65
+ this.resizeObserver = new ResizeObserver(entries => this.update(entries))
66
+ this.resizeObserver.observe(cfg.containerElement)
67
+ }
68
+
69
+ cfg!: Required<FitImagesCfg>
70
+ resizeObserver: ResizeObserver
71
+ containerWidth = -1
72
+
73
+ stop(): void {
74
+ this.resizeObserver.disconnect()
75
+ }
76
+
77
+ private update(entries: ResizeObserverEntry[]): void {
78
+ const width = Math.floor(entries[0]!.contentRect.width)
79
+ if (width === this.containerWidth) return // we're only interested in width changes
80
+ this.containerWidth = width
81
+
82
+ console.log(`resize ${width}`)
83
+ this.doLayout(this.cfg.images)
84
+ this.cfg.onChange(this.cfg.images)
85
+ }
86
+
87
+ private doLayout(imgs: readonly FitImage[]): void {
88
+ if (imgs.length === 0) return // nothing to do
89
+ const { maxHeight } = this.cfg
90
+
91
+ let imgNodes = imgs.slice(0)
92
+
93
+ w: while (imgNodes.length > 0) {
94
+ let slice: FitImage[]
95
+ let h: number
96
+
97
+ for (let i = 1; i <= imgNodes.length; i++) {
98
+ slice = imgNodes.slice(0, i)
99
+ h = this.getHeigth(slice)
100
+
101
+ if (h < maxHeight) {
102
+ this.setHeight(slice, h)
103
+ imgNodes = imgNodes.slice(i)
104
+ continue w
105
+ }
106
+ }
107
+
108
+ this.setHeight(slice!, Math.min(maxHeight, h!))
109
+ break
110
+ }
111
+ }
112
+
113
+ private getHeigth(images: readonly FitImage[]): number {
114
+ const width = this.containerWidth - images.length * this.cfg.margin
115
+ let r = 0
116
+ images.forEach(img => (r += img.aspectRatio))
117
+
118
+ return width / r // have to round down because Firefox will automatically roundup value with number of decimals > 3
119
+ }
120
+
121
+ // mutates/sets images' fitWidth, fitHeight properties
122
+ private setHeight(images: readonly FitImage[], height: number): void {
123
+ images.forEach(img => {
124
+ img.fitWidth = Math.floor(height * img.aspectRatio)
125
+ img.fitHeight = Math.floor(height)
126
+ })
127
+ }
128
+ }
@@ -0,0 +1,52 @@
1
+ import { isServerSide } from '../env'
2
+ import { _objectAssign } from '../types'
3
+
4
+ export type LoadScriptOptions = Partial<HTMLScriptElement>
5
+ export type LoadCSSOptions = Partial<HTMLLinkElement>
6
+
7
+ /**
8
+ * opt.async defaults to `true`.
9
+ * No other options are set by default.
10
+ */
11
+ export async function loadScript(src: string, opt?: LoadScriptOptions): Promise<void> {
12
+ if (isServerSide()) return
13
+
14
+ return await new Promise<void>((resolve, reject) => {
15
+ const s = _objectAssign(document.createElement('script'), {
16
+ src,
17
+ async: true,
18
+ ...opt,
19
+ onload: resolve as any,
20
+ onerror: (_event, _source, _lineno, _colno, err) => {
21
+ reject(err || new Error(`loadScript failed: ${src}`))
22
+ },
23
+ })
24
+ document.head.append(s)
25
+ })
26
+ }
27
+
28
+ /**
29
+ * Default options:
30
+ * rel: 'stylesheet'
31
+ *
32
+ * No other options are set by default.
33
+ */
34
+ export async function loadCSS(href: string, opt?: LoadCSSOptions): Promise<void> {
35
+ if (isServerSide()) return
36
+
37
+ return await new Promise<void>((resolve, reject) => {
38
+ const link = _objectAssign(document.createElement('link'), {
39
+ href,
40
+ rel: 'stylesheet',
41
+ // type seems to be unnecessary: https://stackoverflow.com/a/5409146/4919972
42
+ // type: 'text/css',
43
+ ...opt,
44
+ onload: resolve as any,
45
+ onerror: (_event, _source, _lineno, _colno, err) => {
46
+ reject(err || new Error(`loadCSS failed: ${href}`))
47
+ },
48
+ })
49
+
50
+ document.head.append(link)
51
+ })
52
+ }
@@ -0,0 +1,147 @@
1
+ // Modified version of topbar:
2
+ // http://buunguyen.github.io/topbar
3
+ /* eslint-disable */
4
+
5
+ export interface TopBarOptions {
6
+ /**
7
+ * @default true
8
+ */
9
+ autoRun?: boolean
10
+
11
+ /**
12
+ * @default 5
13
+ */
14
+ barThickness?: number
15
+
16
+ barColors?: any
17
+ shadowColor?: any
18
+
19
+ /**
20
+ * @default 10
21
+ */
22
+ shadowBlur?: number
23
+ }
24
+
25
+ const browser = typeof window !== 'undefined'
26
+
27
+ let canvas: any
28
+ let progressTimerId: any
29
+ let fadeTimerId: any
30
+ let currentProgress: any
31
+ let showing: any
32
+ const addEvent = (elem: any, type: any, handler: any) => {
33
+ if (elem.addEventListener) elem.addEventListener(type, handler, false)
34
+ else if (elem.attachEvent) elem.attachEvent('on' + type, handler)
35
+ else elem['on' + type] = handler
36
+ }
37
+ const options = {
38
+ autoRun: true,
39
+ barThickness: 5,
40
+ barColors: {
41
+ '0': 'rgba(26, 188, 156, .9)',
42
+ '.25': 'rgba(52, 152, 219, .9)',
43
+ '.50': 'rgba(241, 196, 15, .9)',
44
+ '.75': 'rgba(230, 126, 34, .9)',
45
+ '1.0': 'rgba(211, 84, 0, .9)',
46
+ },
47
+ shadowBlur: 10,
48
+ shadowColor: 'rgba(0, 0, 0, .6)',
49
+ }
50
+ const repaint = () => {
51
+ canvas.width = window.innerWidth
52
+ canvas.height = options.barThickness * 5 // need space for shadow
53
+
54
+ const ctx = canvas.getContext('2d')
55
+ ctx.shadowBlur = options.shadowBlur
56
+ ctx.shadowColor = options.shadowColor
57
+
58
+ const lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0)
59
+ for (const stop in options.barColors) {
60
+ // @ts-ignore
61
+ lineGradient.addColorStop(stop, options.barColors[stop])
62
+ }
63
+ ctx.lineWidth = options.barThickness
64
+ ctx.beginPath()
65
+ ctx.moveTo(0, options.barThickness / 2)
66
+ ctx.lineTo(Math.ceil(currentProgress * canvas.width), options.barThickness / 2)
67
+ ctx.strokeStyle = lineGradient
68
+ ctx.stroke()
69
+ }
70
+ const createCanvas = () => {
71
+ canvas = document.createElement('canvas')
72
+ const style = canvas.style
73
+ style.position = 'fixed'
74
+ style.top = style.left = style.right = style.margin = style.padding = 0
75
+ style.zIndex = 100001
76
+ style.display = 'none'
77
+ document.body.appendChild(canvas)
78
+ addEvent(window, 'resize', repaint)
79
+ }
80
+
81
+ export const topbar = {
82
+ config(opts: TopBarOptions) {
83
+ for (const key in opts) {
84
+ if (options.hasOwnProperty(key)) {
85
+ // @ts-ignore
86
+ options[key] = opts[key]
87
+ }
88
+ }
89
+ },
90
+ set(show: boolean, opts?: TopBarOptions) {
91
+ if (show) {
92
+ topbar.show(opts)
93
+ } else {
94
+ topbar.hide()
95
+ }
96
+ },
97
+ show(opts?: TopBarOptions) {
98
+ if (!browser) return // ssr protection
99
+ if (opts) topbar.config(opts)
100
+ if (showing) return
101
+ showing = true
102
+ if (fadeTimerId !== null) {
103
+ window.cancelAnimationFrame(fadeTimerId)
104
+ }
105
+ if (!canvas) createCanvas()
106
+ canvas.style.opacity = 1
107
+ canvas.style.display = 'block'
108
+ topbar.progress(0)
109
+ if (options.autoRun) {
110
+ ;(function loop() {
111
+ progressTimerId = window.requestAnimationFrame(loop)
112
+ topbar.progress('+' + 0.05 * (1 - Math.sqrt(currentProgress)) ** 2)
113
+ })()
114
+ }
115
+ },
116
+ progress(to: number | string) {
117
+ if (!browser) return // ssr protection
118
+ if (typeof to === 'undefined') {
119
+ return currentProgress
120
+ }
121
+ if (typeof to === 'string') {
122
+ to = (to.indexOf('+') >= 0 || to.indexOf('-') >= 0 ? currentProgress : 0) + parseFloat(to)
123
+ }
124
+ currentProgress = (to as number) > 1 ? 1 : to
125
+ repaint()
126
+ return currentProgress
127
+ },
128
+ hide() {
129
+ if (!showing || !browser) return
130
+ showing = false
131
+ if (progressTimerId != null) {
132
+ window.cancelAnimationFrame(progressTimerId)
133
+ progressTimerId = null
134
+ }
135
+ ;(function loop() {
136
+ if (topbar.progress('+.1') >= 1) {
137
+ canvas.style.opacity -= 0.05
138
+ if (canvas.style.opacity <= 0.05) {
139
+ canvas.style.display = 'none'
140
+ fadeTimerId = null
141
+ return
142
+ }
143
+ }
144
+ fadeTimerId = window.requestAnimationFrame(loop)
145
+ })()
146
+ },
147
+ }
@@ -57,9 +57,11 @@ export class LocalDate {
57
57
  setYear(v: number): LocalDate {
58
58
  return this.set('year', v)
59
59
  }
60
+
60
61
  setMonth(v: number): LocalDate {
61
62
  return this.set('month', v)
62
63
  }
64
+
63
65
  setDay(v: number): LocalDate {
64
66
  return this.set('day', v)
65
67
  }
@@ -143,15 +145,19 @@ export class LocalDate {
143
145
  isToday(): boolean {
144
146
  return this.isSame(localDate.today())
145
147
  }
148
+
146
149
  isAfterToday(): boolean {
147
150
  return this.isAfter(localDate.today())
148
151
  }
152
+
149
153
  isSameOrAfterToday(): boolean {
150
154
  return this.isSameOrAfter(localDate.today())
151
155
  }
156
+
152
157
  isBeforeToday(): boolean {
153
158
  return this.isBefore(localDate.today())
154
159
  }
160
+
155
161
  isSameOrBeforeToday(): boolean {
156
162
  return this.isSameOrBefore(localDate.today())
157
163
  }
@@ -159,12 +165,15 @@ export class LocalDate {
159
165
  getAgeInYears(today?: LocalDateInput): number {
160
166
  return this.getAgeIn('year', today)
161
167
  }
168
+
162
169
  getAgeInMonths(today?: LocalDateInput): number {
163
170
  return this.getAgeIn('month', today)
164
171
  }
172
+
165
173
  getAgeInDays(today?: LocalDateInput): number {
166
174
  return this.getAgeIn('day', today)
167
175
  }
176
+
168
177
  getAgeIn(unit: LocalDateUnit, today?: LocalDateInput): number {
169
178
  return localDate.fromInput(today || new Date()).diff(this, unit)
170
179
  }
@@ -264,24 +273,31 @@ export class LocalDate {
264
273
  plusDays(num: number): LocalDate {
265
274
  return this.plus(num, 'day')
266
275
  }
276
+
267
277
  plusWeeks(num: number): LocalDate {
268
278
  return this.plus(num, 'week')
269
279
  }
280
+
270
281
  plusMonths(num: number): LocalDate {
271
282
  return this.plus(num, 'month')
272
283
  }
284
+
273
285
  plusYears(num: number): LocalDate {
274
286
  return this.plus(num, 'year')
275
287
  }
288
+
276
289
  minusDays(num: number): LocalDate {
277
290
  return this.plus(-num, 'day')
278
291
  }
292
+
279
293
  minusWeeks(num: number): LocalDate {
280
294
  return this.plus(-num, 'week')
281
295
  }
296
+
282
297
  minusMonths(num: number): LocalDate {
283
298
  return this.plus(-num, 'month')
284
299
  }
300
+
285
301
  minusYears(num: number): LocalDate {
286
302
  return this.plus(-num, 'year')
287
303
  }
@@ -212,42 +212,55 @@ export class LocalTime {
212
212
  get year(): number {
213
213
  return this.$date.getFullYear()
214
214
  }
215
+
215
216
  setYear(v: number): LocalTime {
216
217
  return this.set('year', v)
217
218
  }
219
+
218
220
  get month(): number {
219
221
  return this.$date.getMonth() + 1
220
222
  }
223
+
221
224
  setMonth(v: number): LocalTime {
222
225
  return this.set('month', v)
223
226
  }
227
+
224
228
  get week(): number {
225
229
  return getWeek(this.$date)
226
230
  }
231
+
227
232
  setWeek(v: number): LocalTime {
228
233
  return this.set('week', v)
229
234
  }
235
+
230
236
  get day(): number {
231
237
  return this.$date.getDate()
232
238
  }
239
+
233
240
  setDay(v: number): LocalTime {
234
241
  return this.set('day', v)
235
242
  }
243
+
236
244
  get hour(): number {
237
245
  return this.$date.getHours()
238
246
  }
247
+
239
248
  setHour(v: number): LocalTime {
240
249
  return this.set('hour', v)
241
250
  }
251
+
242
252
  get minute(): number {
243
253
  return this.$date.getMinutes()
244
254
  }
255
+
245
256
  setMinute(v: number): LocalTime {
246
257
  return this.set('minute', v)
247
258
  }
259
+
248
260
  get second(): number {
249
261
  return this.$date.getSeconds()
250
262
  }
263
+
251
264
  setSecond(v: number): LocalTime {
252
265
  return this.set('second', v)
253
266
  }
@@ -258,6 +271,7 @@ export class LocalTime {
258
271
  get dayOfWeek(): ISODayOfWeek {
259
272
  return (this.$date.getDay() || 7) as ISODayOfWeek
260
273
  }
274
+
261
275
  setDayOfWeek(v: ISODayOfWeek): LocalTime {
262
276
  _assert(VALID_DAYS_OF_WEEK.has(v), `Invalid dayOfWeek: ${v}`)
263
277
  const dow = this.$date.getDay() || 7
@@ -292,42 +306,55 @@ export class LocalTime {
292
306
  plusSeconds(num: number): LocalTime {
293
307
  return this.plus(num, 'second')
294
308
  }
309
+
295
310
  plusMinutes(num: number): LocalTime {
296
311
  return this.plus(num, 'minute')
297
312
  }
313
+
298
314
  plusHours(num: number): LocalTime {
299
315
  return this.plus(num, 'hour')
300
316
  }
317
+
301
318
  plusDays(num: number): LocalTime {
302
319
  return this.plus(num, 'day')
303
320
  }
321
+
304
322
  plusWeeks(num: number): LocalTime {
305
323
  return this.plus(num, 'week')
306
324
  }
325
+
307
326
  plusMonths(num: number): LocalTime {
308
327
  return this.plus(num, 'month')
309
328
  }
329
+
310
330
  plusYears(num: number): LocalTime {
311
331
  return this.plus(num, 'year')
312
332
  }
333
+
313
334
  minusSeconds(num: number): LocalTime {
314
335
  return this.plus(-num, 'second')
315
336
  }
337
+
316
338
  minusMinutes(num: number): LocalTime {
317
339
  return this.plus(-num, 'minute')
318
340
  }
341
+
319
342
  minusHours(num: number): LocalTime {
320
343
  return this.plus(-num, 'hour')
321
344
  }
345
+
322
346
  minusDays(num: number): LocalTime {
323
347
  return this.plus(-num, 'day')
324
348
  }
349
+
325
350
  minusWeeks(num: number): LocalTime {
326
351
  return this.plus(-num, 'week')
327
352
  }
353
+
328
354
  minusMonths(num: number): LocalTime {
329
355
  return this.plus(-num, 'month')
330
356
  }
357
+
331
358
  minusYears(num: number): LocalTime {
332
359
  return this.plus(-num, 'year')
333
360
  }
@@ -452,20 +479,25 @@ export class LocalTime {
452
479
  isSame(d: LocalTimeInput): boolean {
453
480
  return this.compare(d) === 0
454
481
  }
482
+
455
483
  isBefore(d: LocalTimeInput, inclusive = false): boolean {
456
484
  const r = this.compare(d)
457
485
  return r === -1 || (r === 0 && inclusive)
458
486
  }
487
+
459
488
  isSameOrBefore(d: LocalTimeInput): boolean {
460
489
  return this.compare(d) <= 0
461
490
  }
491
+
462
492
  isAfter(d: LocalTimeInput, inclusive = false): boolean {
463
493
  const r = this.compare(d)
464
494
  return r === 1 || (r === 0 && inclusive)
465
495
  }
496
+
466
497
  isSameOrAfter(d: LocalTimeInput): boolean {
467
498
  return this.compare(d) >= 0
468
499
  }
500
+
469
501
  isBetween(min: LocalTimeInput, max: LocalTimeInput, incl: Inclusiveness = '[)'): boolean {
470
502
  let r = this.compare(min)
471
503
  // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
@@ -518,21 +550,27 @@ export class LocalTime {
518
550
  getAgeInYears(now?: LocalTimeInput): number {
519
551
  return this.getAgeIn('year', now)
520
552
  }
553
+
521
554
  getAgeInMonths(now?: LocalTimeInput): number {
522
555
  return this.getAgeIn('month', now)
523
556
  }
557
+
524
558
  getAgeInDays(now?: LocalTimeInput): number {
525
559
  return this.getAgeIn('day', now)
526
560
  }
561
+
527
562
  getAgeInHours(now?: LocalTimeInput): number {
528
563
  return this.getAgeIn('hour', now)
529
564
  }
565
+
530
566
  getAgeInMinutes(now?: LocalTimeInput): number {
531
567
  return this.getAgeIn('minute', now)
532
568
  }
569
+
533
570
  getAgeInSeconds(now?: LocalTimeInput): number {
534
571
  return this.getAgeIn('second', now)
535
572
  }
573
+
536
574
  getAgeIn(unit: LocalTimeUnit, now?: LocalTimeInput): number {
537
575
  return localTime.fromInput(now ?? new Date()).diff(this, unit)
538
576
  }
@@ -540,6 +578,7 @@ export class LocalTime {
540
578
  isAfterNow(): boolean {
541
579
  return this.$date.valueOf() > Date.now()
542
580
  }
581
+
543
582
  isBeforeNow(): boolean {
544
583
  return this.$date.valueOf() < Date.now()
545
584
  }
@@ -161,6 +161,7 @@ export function _debounce<T extends AnyFunction>(
161
161
  }
162
162
  return result
163
163
  }
164
+
164
165
  debounced.cancel = cancel
165
166
  debounced.flush = flush
166
167
  debounced.pending = pending
@@ -1,4 +1,7 @@
1
- import { _isPrimitive, MISS, pDelay, UnixTimestampNumber } from '..'
1
+ import { _isPrimitive } from '../is.util'
2
+ import { pDelay } from '../promise/pDelay'
3
+ import type { UnixTimestampNumber } from '../types'
4
+ import { MISS } from '../types'
2
5
 
3
6
  export type MemoSerializer = (args: any[]) => any
4
7
 
@@ -0,0 +1,47 @@
1
+ import { AnyObject } from '../types'
2
+ import { _getTargetMethodSignature } from './decorator.util'
3
+
4
+ /**
5
+ * Prevents "swarm" of async calls to the same method.
6
+ * Allows max 1 in-flight promise to exist.
7
+ * If more calls appear, while Promise is not resolved yet - same Promise is returned.
8
+ *
9
+ * Does not support `cacheKey`.
10
+ * So, the same Promise is returned, regardless of the arguments.
11
+ */
12
+ // eslint-disable-next-line @typescript-eslint/naming-convention
13
+ export const _SwarmSafe = (): MethodDecorator => (target, key, descriptor) => {
14
+ if (typeof descriptor.value !== 'function') {
15
+ throw new TypeError('@_SwarmSafe can be applied only to methods')
16
+ }
17
+
18
+ const originalFn = descriptor.value
19
+ const keyStr = String(key)
20
+ const methodSignature = _getTargetMethodSignature(target, keyStr)
21
+ const instanceCache = new Map<AnyObject, Promise<any>>()
22
+
23
+ console.log('SwarmSafe constructor called', { key, methodSignature })
24
+
25
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
26
+ descriptor.value = function (this: typeof target, ...args: any[]): Promise<any> {
27
+ console.log('SwarmSafe method called', { key, methodSignature, args })
28
+ const ctx = this
29
+
30
+ let inFlightPromise = instanceCache.get(ctx)
31
+ if (inFlightPromise) {
32
+ console.log(`SwarmSafe: returning in-flight promise`)
33
+ return inFlightPromise
34
+ }
35
+
36
+ console.log(`SwarmSafe: first-time call, creating in-flight promise`)
37
+
38
+ inFlightPromise = originalFn.apply(ctx, args) as Promise<any>
39
+ instanceCache.set(ctx, inFlightPromise)
40
+ void inFlightPromise.finally(() => {
41
+ console.log(`SwarmSafe: in-flight promise resolved`)
42
+ instanceCache.delete(ctx)
43
+ })
44
+
45
+ return inFlightPromise
46
+ } as any
47
+ }