@naturalcycles/js-lib 14.80.1 → 14.83.1

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 (42) hide show
  1. package/dist/decorators/asyncMemo.decorator.d.ts +22 -0
  2. package/dist/decorators/asyncMemo.decorator.js +96 -0
  3. package/dist/decorators/memo.decorator.d.ts +8 -0
  4. package/dist/decorators/memo.decorator.js +11 -6
  5. package/dist/decorators/memo.util.d.ts +27 -8
  6. package/dist/decorators/memo.util.js +1 -1
  7. package/dist/decorators/retry.decorator.js +1 -1
  8. package/dist/error/error.model.d.ts +6 -0
  9. package/dist/index.d.ts +5 -5
  10. package/dist/index.js +3 -2
  11. package/dist/promise/AggregatedError.d.ts +1 -1
  12. package/dist/promise/AggregatedError.js +2 -7
  13. package/dist/promise/pFilter.d.ts +2 -3
  14. package/dist/promise/pFilter.js +4 -4
  15. package/dist/promise/pMap.d.ts +1 -1
  16. package/dist/promise/pMap.js +67 -19
  17. package/dist/promise/pRetry.d.ts +17 -2
  18. package/dist/promise/pRetry.js +72 -40
  19. package/dist-esm/decorators/asyncMemo.decorator.js +104 -0
  20. package/dist-esm/decorators/memo.decorator.js +11 -5
  21. package/dist-esm/decorators/memo.util.js +1 -1
  22. package/dist-esm/decorators/retry.decorator.js +2 -2
  23. package/dist-esm/index.js +3 -3
  24. package/dist-esm/promise/AggregatedError.js +2 -7
  25. package/dist-esm/promise/pFilter.js +4 -4
  26. package/dist-esm/promise/pMap.js +79 -19
  27. package/dist-esm/promise/pRetry.js +70 -39
  28. package/package.json +1 -1
  29. package/src/decorators/asyncMemo.decorator.ts +151 -0
  30. package/src/decorators/memo.decorator.ts +16 -5
  31. package/src/decorators/memo.util.ts +36 -10
  32. package/src/decorators/retry.decorator.ts +2 -2
  33. package/src/error/error.model.ts +7 -0
  34. package/src/index.ts +5 -3
  35. package/src/promise/AggregatedError.ts +3 -8
  36. package/src/promise/pFilter.ts +5 -14
  37. package/src/promise/pMap.ts +72 -21
  38. package/src/promise/pRetry.ts +105 -41
  39. package/dist/promise/pBatch.d.ts +0 -7
  40. package/dist/promise/pBatch.js +0 -30
  41. package/dist-esm/promise/pBatch.js +0 -23
  42. package/src/promise/pBatch.ts +0 -31
@@ -0,0 +1,151 @@
1
+ import { _since } from '../time/time.util'
2
+ import { Merge } from '../typeFest'
3
+ import { AnyObject } from '../types'
4
+ import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util'
5
+ import { CACHE_DROP, MemoOptions } from './memo.decorator'
6
+ import { AsyncMemoCache, jsonMemoSerializer } from './memo.util'
7
+
8
+ export type AsyncMemoOptions = Merge<
9
+ MemoOptions,
10
+ {
11
+ /**
12
+ * Provide a custom implementation of MemoCache.
13
+ * Function that creates an instance of `MemoCache`.
14
+ * e.g LRUMemoCache from `@naturalcycles/nodejs-lib`.
15
+ *
16
+ * It's an ARRAY of Caches, to allow multiple layers of Cache.
17
+ * It will check it one by one, starting from the first.
18
+ * HIT will be returned immediately, MISS will go one level deeper, or returned (if the end of the Cache stack is reached).
19
+ */
20
+ cacheFactory: () => AsyncMemoCache[]
21
+ }
22
+ >
23
+
24
+ /**
25
+ * Like @_Memo, but allowing async MemoCache implementation.
26
+ *
27
+ * Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
28
+ * Return `null` instead (it'll be cached).
29
+ */
30
+ // eslint-disable-next-line @typescript-eslint/naming-convention
31
+ export const _AsyncMemo =
32
+ (opt: AsyncMemoOptions): MethodDecorator =>
33
+ (target, key, descriptor) => {
34
+ if (typeof descriptor.value !== 'function') {
35
+ throw new TypeError('Memoization can be applied only to methods')
36
+ }
37
+
38
+ const originalFn = descriptor.value
39
+
40
+ // Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
41
+ const cache = new Map<AnyObject, AsyncMemoCache[]>()
42
+
43
+ const {
44
+ logHit = false,
45
+ logMiss = false,
46
+ noLogArgs = false,
47
+ logger = console,
48
+ cacheFactory,
49
+ cacheKeyFn = jsonMemoSerializer,
50
+ noCacheRejected = false,
51
+ noCacheResolved = false,
52
+ } = opt
53
+
54
+ const keyStr = String(key)
55
+ const methodSignature = _getTargetMethodSignature(target, keyStr)
56
+
57
+ descriptor.value = async function (this: typeof target, ...args: any[]): Promise<any> {
58
+ const ctx = this
59
+
60
+ const cacheKey = cacheKeyFn(args)
61
+
62
+ if (!cache.has(ctx)) {
63
+ cache.set(ctx, cacheFactory())
64
+ // here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created
65
+ // UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache)
66
+ }
67
+
68
+ if (args.length === 1 && args[0] === CACHE_DROP) {
69
+ // Special event - CACHE_DROP
70
+ // Function will return undefined
71
+ logger.log(`${methodSignature} @_AsyncMemo.dropCache()`)
72
+ try {
73
+ await Promise.all(cache.get(ctx)!.map(c => c.clear()))
74
+ } catch (err) {
75
+ logger.error(err)
76
+ }
77
+
78
+ return
79
+ }
80
+
81
+ let value: any
82
+
83
+ try {
84
+ for await (const cacheLayer of cache.get(ctx)!) {
85
+ value = await cacheLayer.get(cacheKey)
86
+ if (value !== undefined) {
87
+ // it's a hit!
88
+ break
89
+ }
90
+ }
91
+ } catch (err) {
92
+ // log error, but don't throw, treat it as a "miss"
93
+ logger.error(err)
94
+ }
95
+
96
+ if (value !== undefined) {
97
+ // hit!
98
+ if (logHit) {
99
+ logger.log(
100
+ `${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(
101
+ args,
102
+ noLogArgs,
103
+ )}) @_AsyncMemo hit`,
104
+ )
105
+ }
106
+
107
+ return value instanceof Error ? Promise.reject(value) : Promise.resolve(value)
108
+ }
109
+
110
+ // Here we know it's a MISS, let's execute the real method
111
+ const started = Date.now()
112
+
113
+ try {
114
+ value = await originalFn.apply(ctx, args)
115
+
116
+ if (!noCacheResolved) {
117
+ Promise.all(cache.get(ctx)!.map(cacheLayer => cacheLayer.set(cacheKey, value))).catch(
118
+ err => {
119
+ // log and ignore the error
120
+ logger.error(err)
121
+ },
122
+ )
123
+ }
124
+
125
+ return value
126
+ } catch (err) {
127
+ if (!noCacheRejected) {
128
+ // We put it to cache as raw Error, not Promise.reject(err)
129
+ Promise.all(cache.get(ctx)!.map(cacheLayer => cacheLayer.set(cacheKey, err))).catch(
130
+ err => {
131
+ // log and ignore the error
132
+ logger.error(err)
133
+ },
134
+ )
135
+ }
136
+
137
+ throw err
138
+ } finally {
139
+ if (logMiss) {
140
+ logger.log(
141
+ `${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(
142
+ args,
143
+ noLogArgs,
144
+ )}) @_AsyncMemo miss (${_since(started)})`,
145
+ )
146
+ }
147
+ }
148
+ } as any
149
+
150
+ return descriptor
151
+ }
@@ -10,6 +10,11 @@ import { AnyObject } from '../types'
10
10
  import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util'
11
11
  import { jsonMemoSerializer, MapMemoCache, MemoCache } from './memo.util'
12
12
 
13
+ /**
14
+ * Symbol to indicate that the Cache should be dropped.
15
+ */
16
+ export const CACHE_DROP = Symbol('CACHE_DROP')
17
+
13
18
  export interface MemoOptions {
14
19
  /**
15
20
  * Default to false
@@ -45,12 +50,16 @@ export interface MemoOptions {
45
50
  /**
46
51
  * Don't cache resolved promises.
47
52
  * Setting this to `true` will make the decorator to await the result.
53
+ *
54
+ * Default false.
48
55
  */
49
56
  noCacheResolved?: boolean
50
57
 
51
58
  /**
52
59
  * Don't cache rejected promises.
53
60
  * Setting this to `true` will make the decorator to await the result.
61
+ *
62
+ * Default false.
54
63
  */
55
64
  noCacheRejected?: boolean
56
65
  }
@@ -102,6 +111,13 @@ export const _Memo =
102
111
  descriptor.value = function (this: typeof target, ...args: any[]): any {
103
112
  const ctx = this
104
113
 
114
+ if (args.length === 1 && args[0] === CACHE_DROP) {
115
+ // Special event - CACHE_DROP
116
+ // Function will return undefined
117
+ logger.log(`${methodSignature} @_Memo.CACHE_DROP`)
118
+ return cache.get(ctx)?.clear()
119
+ }
120
+
105
121
  const cacheKey = cacheKeyFn(args)
106
122
 
107
123
  if (!cache.has(ctx)) {
@@ -179,11 +195,6 @@ export const _Memo =
179
195
  return res
180
196
  }
181
197
  } as any
182
- ;(descriptor.value as any).dropCache = () => {
183
- logger.log(`${methodSignature} @_Memo.dropCache()`)
184
- cache.forEach(memoCache => memoCache.clear())
185
- cache.clear()
186
- }
187
198
 
188
199
  return descriptor
189
200
  }
@@ -1,20 +1,44 @@
1
1
  import { _isPrimitive } from '../object/object.util'
2
+ import { Promisable } from '../typeFest'
2
3
 
3
4
  export type MemoSerializer = (args: any[]) => any
4
5
 
5
6
  export const jsonMemoSerializer: MemoSerializer = args => {
6
- if (!args.length) return undefined
7
+ if (args.length === 0) return undefined
7
8
  if (args.length === 1 && _isPrimitive(args[0])) return args[0]
8
9
  return JSON.stringify(args)
9
10
  }
10
11
 
11
- export interface MemoCache {
12
- has(k: any): boolean
13
- get(k: any): any
14
- set(k: any, v: any): void
12
+ export interface MemoCache<KEY = any, VALUE = any> {
13
+ has(k: KEY): boolean
14
+ get(k: KEY): VALUE | undefined
15
+ set(k: KEY, v: VALUE): void
16
+
17
+ /**
18
+ * Clear is only called when `.dropCache()` is called.
19
+ * Otherwise the Cache is "persistent" (never cleared).
20
+ */
15
21
  clear(): void
16
22
  }
17
23
 
24
+ export interface AsyncMemoCache<KEY = any, VALUE = any> {
25
+ // `has` method is removed, because it is assumed that it has a cost and it's best to avoid doing both `has` and then `get`
26
+ // has(k: any): Promise<boolean>
27
+ /**
28
+ * `undefined` value returned indicates the ABSENCE of value in the Cache.
29
+ * This also means that you CANNOT store `undefined` value in the Cache, as it'll be treated as a MISS.
30
+ * You CAN store `null` value instead, it will be treated as a HIT.
31
+ */
32
+ get(k: KEY): Promisable<VALUE | undefined>
33
+ set(k: KEY, v: VALUE): Promisable<void>
34
+
35
+ /**
36
+ * Clear is only called when `.dropCache()` is called.
37
+ * Otherwise the Cache is "persistent" (never cleared).
38
+ */
39
+ clear(): Promisable<void>
40
+ }
41
+
18
42
  // SingleValueMemoCache and ObjectMemoCache are example-only, not used in production code
19
43
  /*
20
44
  export class SingleValueMemoCache implements MemoCache {
@@ -61,18 +85,20 @@ export class ObjectMemoCache implements MemoCache {
61
85
  }
62
86
  */
63
87
 
64
- export class MapMemoCache implements MemoCache {
65
- private m = new Map<any, any>()
88
+ export class MapMemoCache<KEY = any, VALUE = any>
89
+ implements MemoCache<KEY, VALUE>, AsyncMemoCache<KEY, VALUE>
90
+ {
91
+ private m = new Map<KEY, VALUE>()
66
92
 
67
- has(k: any): boolean {
93
+ has(k: KEY): boolean {
68
94
  return this.m.has(k)
69
95
  }
70
96
 
71
- get(k: any): any {
97
+ get(k: KEY): VALUE | undefined {
72
98
  return this.m.get(k)
73
99
  }
74
100
 
75
- set(k: any, v: any): void {
101
+ set(k: KEY, v: VALUE): void {
76
102
  this.m.set(k, v)
77
103
  }
78
104
 
@@ -1,10 +1,10 @@
1
- import { pRetry, PRetryOptions } from '..'
1
+ import { pRetryFn, PRetryOptions } from '..'
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/naming-convention
4
4
  export function _Retry(opt: PRetryOptions = {}): MethodDecorator {
5
5
  return (target, key, descriptor) => {
6
6
  const originalFn = descriptor.value
7
- descriptor.value = pRetry(originalFn as any, opt)
7
+ descriptor.value = pRetryFn(originalFn as any, opt)
8
8
  return descriptor
9
9
  }
10
10
  }
@@ -31,6 +31,13 @@ export interface ErrorData {
31
31
  */
32
32
  originalMessage?: string
33
33
 
34
+ /**
35
+ * Can be used by error-reporting tools (e.g Sentry).
36
+ * If fingerprint is defined - it'll be used INSTEAD of default fingerprint of a tool.
37
+ * Can be used to force-group errors that are NOT needed to be split by endpoint or calling function.
38
+ */
39
+ fingerprint?: string[]
40
+
34
41
  /**
35
42
  * Open-ended.
36
43
  */
package/src/index.ts CHANGED
@@ -12,7 +12,8 @@ export * from './decorators/debounce.decorator'
12
12
  export * from './decorators/decorator.util'
13
13
  export * from './decorators/logMethod.decorator'
14
14
  export * from './decorators/memo.decorator'
15
- import { MemoCache } from './decorators/memo.util'
15
+ export * from './decorators/asyncMemo.decorator'
16
+ import { MemoCache, AsyncMemoCache } from './decorators/memo.util'
16
17
  export * from './decorators/memoFn'
17
18
  export * from './decorators/retry.decorator'
18
19
  export * from './decorators/timeout.decorator'
@@ -67,14 +68,13 @@ export * from './object/object.util'
67
68
  export * from './object/sortObject'
68
69
  export * from './object/sortObjectDeep'
69
70
  import { AggregatedError } from './promise/AggregatedError'
70
- export * from './promise/pBatch'
71
71
  import { DeferredPromise, pDefer } from './promise/pDefer'
72
72
  export * from './promise/pDelay'
73
73
  export * from './promise/pFilter'
74
74
  export * from './promise/pHang'
75
75
  import { pMap, PMapOptions } from './promise/pMap'
76
76
  export * from './promise/pProps'
77
- import { pRetry, PRetryOptions } from './promise/pRetry'
77
+ import { pRetry, pRetryFn, PRetryOptions } from './promise/pRetry'
78
78
  export * from './promise/pState'
79
79
  import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout'
80
80
  export * from './promise/pTuple'
@@ -161,6 +161,7 @@ export type {
161
161
  AbortableAsyncMapper,
162
162
  PQueueCfg,
163
163
  MemoCache,
164
+ AsyncMemoCache,
164
165
  PromiseDecoratorCfg,
165
166
  PromiseDecoratorResp,
166
167
  ErrorData,
@@ -250,6 +251,7 @@ export {
250
251
  pDefer,
251
252
  AggregatedError,
252
253
  pRetry,
254
+ pRetryFn,
253
255
  pTimeout,
254
256
  pTimeoutFn,
255
257
  _tryCatch,
@@ -7,20 +7,15 @@ export class AggregatedError<RESULT = any> extends Error {
7
7
  errors!: Error[]
8
8
  results!: RESULT[]
9
9
 
10
- constructor(errors: (Error | string)[], results: RESULT[] = []) {
11
- const mappedErrors = errors.map(e => {
12
- if (typeof e === 'string') return new Error(e)
13
- return e
14
- })
15
-
10
+ constructor(errors: Error[], results: RESULT[] = []) {
16
11
  const message = [
17
12
  `${errors.length} errors:`,
18
- ...mappedErrors.map((e, i) => `${i + 1}. ${e.message}`),
13
+ ...errors.map((e, i) => `${i + 1}. ${e.message}`),
19
14
  ].join('\n')
20
15
 
21
16
  super(message)
22
17
 
23
- this.errors = mappedErrors
18
+ this.errors = errors
24
19
  this.results = results
25
20
 
26
21
  Object.defineProperty(this, 'name', {
@@ -1,16 +1,7 @@
1
- import { AbortableAsyncPredicate } from '../types'
2
- import { pMap, PMapOptions } from './pMap'
1
+ import { AsyncPredicate } from '../types'
3
2
 
4
- export async function pFilter<T>(
5
- iterable: Iterable<T | PromiseLike<T>>,
6
- filterFn: AbortableAsyncPredicate<T>,
7
- opt?: PMapOptions,
8
- ): Promise<T[]> {
9
- const values = await pMap(
10
- iterable,
11
- async (item, index) => await Promise.all([filterFn(item, index), item]),
12
- opt,
13
- )
14
-
15
- return values.filter(value => Boolean(value[0])).map(value => value[1])
3
+ export async function pFilter<T>(iterable: Iterable<T>, filterFn: AsyncPredicate<T>): Promise<T[]> {
4
+ const items = [...iterable]
5
+ const predicates = await Promise.all(items.map((item, i) => filterFn(item, i)))
6
+ return items.filter((item, i) => predicates[i])
16
7
  }
@@ -55,36 +55,85 @@ export interface PMapOptions {
55
55
  * })();
56
56
  */
57
57
  export async function pMap<IN, OUT>(
58
- iterable: Iterable<IN | PromiseLike<IN>>,
58
+ iterable: Iterable<IN>,
59
59
  mapper: AbortableAsyncMapper<IN, OUT>,
60
60
  opt: PMapOptions = {},
61
61
  ): Promise<OUT[]> {
62
- return new Promise<OUT[]>((resolve, reject) => {
63
- const { concurrency = Number.POSITIVE_INFINITY, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt
62
+ const ret: (OUT | typeof SKIP)[] = []
63
+ // const iterator = iterable[Symbol.iterator]()
64
+ const items = [...iterable]
65
+ const itemsLength = items.length
66
+ if (itemsLength === 0) return [] // short circuit
67
+
68
+ const { concurrency = itemsLength, errorMode = ErrorMode.THROW_IMMEDIATELY } = opt
69
+
70
+ const errors: Error[] = []
71
+ let isSettled = false
72
+ let resolvingCount = 0
73
+ let currentIndex = 0
74
+
75
+ // Special cases that are able to preserve async stack traces
76
+
77
+ if (concurrency === 1) {
78
+ // Special case for concurrency == 1
79
+
80
+ for await (const item of items) {
81
+ try {
82
+ const r = await mapper(item, currentIndex++)
83
+ if (r === END) break
84
+ if (r !== SKIP) ret.push(r)
85
+ } catch (err) {
86
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) throw err
87
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
88
+ errors.push(err as Error)
89
+ }
90
+ // otherwise, suppress completely
91
+ }
92
+ }
93
+
94
+ if (errors.length) {
95
+ throw new AggregatedError(errors, ret)
96
+ }
97
+
98
+ return ret as OUT[]
99
+ } else if (!opt.concurrency || items.length <= opt.concurrency) {
100
+ // Special case for concurrency == infinity or iterable.length < concurrency
101
+
102
+ if (errorMode === ErrorMode.THROW_IMMEDIATELY) {
103
+ return (await Promise.all(items.map((item, i) => mapper(item, i)))).filter(
104
+ r => r !== SKIP && r !== END,
105
+ ) as OUT[]
106
+ }
107
+
108
+ ;(await Promise.allSettled(items.map((item, i) => mapper(item, i)))).forEach(r => {
109
+ if (r.status === 'fulfilled') {
110
+ if (r.value !== SKIP && r.value !== END) ret.push(r.value)
111
+ } else if (errorMode === ErrorMode.THROW_AGGREGATED) {
112
+ errors.push(r.reason)
113
+ }
114
+ })
115
+
116
+ if (errors.length) {
117
+ throw new AggregatedError(errors, ret)
118
+ }
64
119
 
65
- const ret: (OUT | typeof SKIP)[] = []
66
- const iterator = iterable[Symbol.iterator]()
67
- const errors: Error[] = []
68
- let isSettled = false
69
- let isIterableDone = false
70
- let resolvingCount = 0
71
- let currentIndex = 0
120
+ return ret as OUT[]
121
+ }
72
122
 
73
- const next = (skipped = false) => {
123
+ return new Promise<OUT[]>((resolve, reject) => {
124
+ const next = () => {
74
125
  if (isSettled) {
75
126
  return
76
127
  }
77
128
 
78
- const nextItem = iterator.next()
79
- const i = currentIndex
80
- if (!skipped) currentIndex++
81
-
82
- if (nextItem.done) {
83
- isIterableDone = true
129
+ const nextItem = items[currentIndex]!
130
+ const i = currentIndex++
84
131
 
132
+ if (currentIndex > itemsLength) {
85
133
  if (resolvingCount === 0) {
134
+ isSettled = true
86
135
  const r = ret.filter(r => r !== SKIP) as OUT[]
87
- if (errors.length && errorMode === ErrorMode.THROW_AGGREGATED) {
136
+ if (errors.length) {
88
137
  reject(new AggregatedError(errors, r))
89
138
  } else {
90
139
  resolve(r)
@@ -96,7 +145,7 @@ export async function pMap<IN, OUT>(
96
145
 
97
146
  resolvingCount++
98
147
 
99
- Promise.resolve(nextItem.value)
148
+ Promise.resolve(nextItem)
100
149
  .then(async element => await mapper(element, i))
101
150
  .then(
102
151
  value => {
@@ -114,7 +163,9 @@ export async function pMap<IN, OUT>(
114
163
  isSettled = true
115
164
  reject(err)
116
165
  } else {
117
- errors.push(err)
166
+ if (errorMode === ErrorMode.THROW_AGGREGATED) {
167
+ errors.push(err)
168
+ }
118
169
  resolvingCount--
119
170
  next()
120
171
  }
@@ -125,7 +176,7 @@ export async function pMap<IN, OUT>(
125
176
  for (let i = 0; i < concurrency; i++) {
126
177
  next()
127
178
 
128
- if (isIterableDone) {
179
+ if (isSettled) {
129
180
  break
130
181
  }
131
182
  }