@naturalcycles/js-lib 15.93.1 → 15.93.3
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/dist/datetime/localTime.js +1 -2
- package/dist/intl/intl.d.ts +15 -0
- package/dist/intl/intl.js +45 -8
- package/dist/promise/pGradualQueue.d.ts +7 -1
- package/dist/promise/pGradualQueue.js +40 -33
- package/dist/promise/pQueue.d.ts +7 -1
- package/dist/promise/pQueue.js +56 -49
- package/dist/semver.d.ts +2 -0
- package/dist/semver.js +2 -0
- package/package.json +1 -1
- package/src/datetime/localTime.ts +1 -2
- package/src/intl/intl.ts +45 -9
- package/src/promise/pGradualQueue.ts +48 -39
- package/src/promise/pQueue.ts +62 -51
- package/src/semver.ts +2 -0
|
@@ -868,8 +868,7 @@ class LocalTimeFactory {
|
|
|
868
868
|
isTimezoneValid(tz) {
|
|
869
869
|
if (tz === 'UTC')
|
|
870
870
|
return true; // we deliberately consider UTC a valid timezone, while it's mostly used in testing
|
|
871
|
-
|
|
872
|
-
return Intl.supportedValuesOf('timeZone').includes(tz);
|
|
871
|
+
return Intl2.getSupportedTimezones().includes(tz);
|
|
873
872
|
}
|
|
874
873
|
sort(items, dir = 'asc', opt = {}) {
|
|
875
874
|
const cmp = comparators.by((lt) => lt.$date.valueOf(), { dir });
|
package/dist/intl/intl.d.ts
CHANGED
|
@@ -7,13 +7,28 @@ import type { IANATimezone } from '../types.js';
|
|
|
7
7
|
*
|
|
8
8
|
* Methods accept non-optional arguments consciously,
|
|
9
9
|
* to be able to cache them better. Just pass {} for options.
|
|
10
|
+
*
|
|
11
|
+
* The cache is never evicted, it grows with each unique combination of arguments.
|
|
12
|
+
* Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
|
|
10
13
|
*/
|
|
11
14
|
declare class MemoizedIntl {
|
|
12
15
|
/**
|
|
13
16
|
* Returns the IANA timezone e.g `Europe/Stockholm`.
|
|
14
17
|
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
|
18
|
+
*
|
|
19
|
+
* Memoized, so it returns the timezone at the time of the first call.
|
|
20
|
+
* Changing `process.env.TZ` at runtime after that is not reflected.
|
|
15
21
|
*/
|
|
16
22
|
getTimezone(): IANATimezone;
|
|
23
|
+
/**
|
|
24
|
+
* Returns the list of IANA timezones supported by the runtime (~420 entries).
|
|
25
|
+
* Memoized, because V8 rebuilds the list on every call (~70µs).
|
|
26
|
+
*
|
|
27
|
+
* The list contains canonical CLDR ids only, so valid aliases are missing from it,
|
|
28
|
+
* e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
|
|
29
|
+
* Don't use it to validate user input as is.
|
|
30
|
+
*/
|
|
31
|
+
getSupportedTimezones(): readonly IANATimezone[];
|
|
17
32
|
DateTimeFormat(locales: Intl.LocalesArgument, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
|
|
18
33
|
RelativeTimeFormat(locales: Intl.LocalesArgument, options: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat;
|
|
19
34
|
DurationFormat(locales: Intl.LocalesArgument, options: Intl.DurationFormatOptions): Intl.DurationFormat;
|
package/dist/intl/intl.js
CHANGED
|
@@ -9,15 +9,32 @@ import { _Memo } from '../decorators/memo.decorator.js';
|
|
|
9
9
|
*
|
|
10
10
|
* Methods accept non-optional arguments consciously,
|
|
11
11
|
* to be able to cache them better. Just pass {} for options.
|
|
12
|
+
*
|
|
13
|
+
* The cache is never evicted, it grows with each unique combination of arguments.
|
|
14
|
+
* Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
|
|
12
15
|
*/
|
|
13
16
|
class MemoizedIntl {
|
|
14
17
|
/**
|
|
15
18
|
* Returns the IANA timezone e.g `Europe/Stockholm`.
|
|
16
19
|
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
|
20
|
+
*
|
|
21
|
+
* Memoized, so it returns the timezone at the time of the first call.
|
|
22
|
+
* Changing `process.env.TZ` at runtime after that is not reflected.
|
|
17
23
|
*/
|
|
18
24
|
getTimezone() {
|
|
19
25
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
20
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the list of IANA timezones supported by the runtime (~420 entries).
|
|
29
|
+
* Memoized, because V8 rebuilds the list on every call (~70µs).
|
|
30
|
+
*
|
|
31
|
+
* The list contains canonical CLDR ids only, so valid aliases are missing from it,
|
|
32
|
+
* e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
|
|
33
|
+
* Don't use it to validate user input as is.
|
|
34
|
+
*/
|
|
35
|
+
getSupportedTimezones() {
|
|
36
|
+
return Intl.supportedValuesOf('timeZone');
|
|
37
|
+
}
|
|
21
38
|
DateTimeFormat(locales, options) {
|
|
22
39
|
return new Intl.DateTimeFormat(locales, options);
|
|
23
40
|
}
|
|
@@ -51,29 +68,49 @@ __decorate([
|
|
|
51
68
|
], MemoizedIntl.prototype, "getTimezone", null);
|
|
52
69
|
__decorate([
|
|
53
70
|
_Memo()
|
|
71
|
+
], MemoizedIntl.prototype, "getSupportedTimezones", null);
|
|
72
|
+
__decorate([
|
|
73
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
54
74
|
], MemoizedIntl.prototype, "DateTimeFormat", null);
|
|
55
75
|
__decorate([
|
|
56
|
-
_Memo()
|
|
76
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
57
77
|
], MemoizedIntl.prototype, "RelativeTimeFormat", null);
|
|
58
78
|
__decorate([
|
|
59
|
-
_Memo()
|
|
79
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
60
80
|
], MemoizedIntl.prototype, "DurationFormat", null);
|
|
61
81
|
__decorate([
|
|
62
|
-
_Memo()
|
|
82
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
63
83
|
], MemoizedIntl.prototype, "NumberFormat", null);
|
|
64
84
|
__decorate([
|
|
65
|
-
_Memo()
|
|
85
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
66
86
|
], MemoizedIntl.prototype, "Collator", null);
|
|
67
87
|
__decorate([
|
|
68
|
-
_Memo()
|
|
88
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
69
89
|
], MemoizedIntl.prototype, "PluralRules", null);
|
|
70
90
|
__decorate([
|
|
71
|
-
_Memo()
|
|
91
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
72
92
|
], MemoizedIntl.prototype, "ListFormat", null);
|
|
73
93
|
__decorate([
|
|
74
|
-
_Memo()
|
|
94
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
75
95
|
], MemoizedIntl.prototype, "DisplayNames", null);
|
|
76
96
|
__decorate([
|
|
77
|
-
_Memo()
|
|
97
|
+
_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
78
98
|
], MemoizedIntl.prototype, "Segmenter", null);
|
|
79
99
|
export const Intl2 = new MemoizedIntl();
|
|
100
|
+
/**
|
|
101
|
+
* Default `jsonMemoSerializer` would serialize `Intl.Locale` instances as `{}`,
|
|
102
|
+
* making all Locale arguments share one cache key. Convert them to their string tags instead.
|
|
103
|
+
*
|
|
104
|
+
* Options keys are serialized in sorted order, so that `{ a, b }` and `{ b, a }` share one cache key.
|
|
105
|
+
* Passing a keys array as replacer works, because Intl options are flat objects.
|
|
106
|
+
*/
|
|
107
|
+
function intlCacheKeyFn([locales, options]) {
|
|
108
|
+
return JSON.stringify(localesKey(locales)) + JSON.stringify(options, Object.keys(options).sort());
|
|
109
|
+
}
|
|
110
|
+
function localesKey(locales) {
|
|
111
|
+
if (locales === undefined)
|
|
112
|
+
return;
|
|
113
|
+
if (Array.isArray(locales))
|
|
114
|
+
return locales.map(String);
|
|
115
|
+
return String(locales);
|
|
116
|
+
}
|
|
@@ -52,7 +52,13 @@ export declare class PGradualQueue {
|
|
|
52
52
|
* Push PromiseReturningFunction to the Queue.
|
|
53
53
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
54
54
|
*/
|
|
55
|
-
push<R>(
|
|
55
|
+
push<R>(fn: AsyncFunction<R>): Promise<R>;
|
|
56
|
+
/**
|
|
57
|
+
* Starts the job and settles its `defer`.
|
|
58
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
59
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
60
|
+
*/
|
|
61
|
+
private start;
|
|
56
62
|
get queueSize(): number;
|
|
57
63
|
/**
|
|
58
64
|
* Current concurrency limit based on warmup progress.
|
|
@@ -48,48 +48,55 @@ export class PGradualQueue {
|
|
|
48
48
|
* Push PromiseReturningFunction to the Queue.
|
|
49
49
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
50
50
|
*/
|
|
51
|
-
async push(
|
|
51
|
+
async push(fn) {
|
|
52
52
|
// Initialize start time on first push
|
|
53
53
|
if (this.startTime === 0) {
|
|
54
54
|
this.startTime = Date.now();
|
|
55
55
|
}
|
|
56
|
-
const {
|
|
57
|
-
const fn = fn_;
|
|
58
|
-
fn.defer ||= pDefer();
|
|
56
|
+
const job = { fn, defer: pDefer() };
|
|
59
57
|
const concurrency = this.getCurrentConcurrency();
|
|
60
58
|
if (this.inFlight < concurrency) {
|
|
61
|
-
this.
|
|
62
|
-
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
63
|
-
runSafe(fn)
|
|
64
|
-
.then(result => {
|
|
65
|
-
fn.defer.resolve(result);
|
|
66
|
-
})
|
|
67
|
-
.catch((err) => {
|
|
68
|
-
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
69
|
-
logger.error(err);
|
|
70
|
-
fn.defer.resolve(); // resolve with `void`
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
fn.defer.reject(err);
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
.finally(() => {
|
|
77
|
-
this.inFlight--;
|
|
78
|
-
const currentConcurrency = this.getCurrentConcurrency();
|
|
79
|
-
logger.debug(`inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`);
|
|
80
|
-
// Start queued jobs up to the current concurrency limit
|
|
81
|
-
// Use while loop since concurrency may have increased during warmup
|
|
82
|
-
while (this.queue.length && this.inFlight < this.getCurrentConcurrency()) {
|
|
83
|
-
const nextFn = this.queue.shift();
|
|
84
|
-
void this.push(nextFn);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
59
|
+
this.start(job, concurrency);
|
|
87
60
|
}
|
|
88
61
|
else {
|
|
89
|
-
this.queue.push(
|
|
90
|
-
logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
|
|
62
|
+
this.queue.push(job);
|
|
63
|
+
this.logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
|
|
91
64
|
}
|
|
92
|
-
return await
|
|
65
|
+
return await job.defer;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Starts the job and settles its `defer`.
|
|
69
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
70
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
71
|
+
*/
|
|
72
|
+
start(job, concurrency) {
|
|
73
|
+
const { logger } = this;
|
|
74
|
+
const { defer } = job;
|
|
75
|
+
this.inFlight++;
|
|
76
|
+
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
77
|
+
runSafe(job.fn)
|
|
78
|
+
.then(result => {
|
|
79
|
+
defer.resolve(result);
|
|
80
|
+
})
|
|
81
|
+
.catch((err) => {
|
|
82
|
+
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
83
|
+
logger.error(err);
|
|
84
|
+
defer.resolve(); // resolve with `void`
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
defer.reject(err);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.finally(() => {
|
|
91
|
+
this.inFlight--;
|
|
92
|
+
const currentConcurrency = this.getCurrentConcurrency();
|
|
93
|
+
logger.debug(`inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`);
|
|
94
|
+
// Start queued jobs up to the current concurrency limit
|
|
95
|
+
// Use while loop since concurrency may have increased during warmup
|
|
96
|
+
while (this.queue.length && this.inFlight < currentConcurrency) {
|
|
97
|
+
this.start(this.queue.shift(), currentConcurrency);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
93
100
|
}
|
|
94
101
|
get queueSize() {
|
|
95
102
|
return this.queue.length;
|
package/dist/promise/pQueue.d.ts
CHANGED
|
@@ -49,7 +49,13 @@ export declare class PQueue {
|
|
|
49
49
|
* Push PromiseReturningFunction to the Queue.
|
|
50
50
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
51
51
|
*/
|
|
52
|
-
push<R>(
|
|
52
|
+
push<R>(fn: AsyncFunction<R>): Promise<R>;
|
|
53
|
+
/**
|
|
54
|
+
* Starts the job and settles its `defer`.
|
|
55
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
56
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
57
|
+
*/
|
|
58
|
+
private start;
|
|
53
59
|
get queueSize(): number;
|
|
54
60
|
/**
|
|
55
61
|
* Returns a Promise that resolves when the queue is Idle (next time, since the call).
|
package/dist/promise/pQueue.js
CHANGED
|
@@ -28,59 +28,66 @@ export class PQueue {
|
|
|
28
28
|
* Push PromiseReturningFunction to the Queue.
|
|
29
29
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
30
30
|
*/
|
|
31
|
-
async push(
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
const fn = fn_;
|
|
35
|
-
fn.defer ||= pDefer();
|
|
36
|
-
if (this.inFlight < concurrency) {
|
|
31
|
+
async push(fn) {
|
|
32
|
+
const job = { fn, defer: pDefer() };
|
|
33
|
+
if (this.inFlight < this.cfg.concurrency) {
|
|
37
34
|
// There is room for more jobs. Can start immediately
|
|
38
|
-
this.
|
|
39
|
-
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
40
|
-
if (resolveOnStart)
|
|
41
|
-
fn.defer.resolve();
|
|
42
|
-
runSafe(fn)
|
|
43
|
-
.then(result => {
|
|
44
|
-
if (!resolveOnStart)
|
|
45
|
-
fn.defer.resolve(result);
|
|
46
|
-
})
|
|
47
|
-
.catch((err) => {
|
|
48
|
-
if (resolveOnStart) {
|
|
49
|
-
logger.error(err);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
53
|
-
logger.error(err);
|
|
54
|
-
fn.defer.resolve(); // resolve with `void`
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Should be handled on the outside, otherwise it'll cause UnhandledRejection
|
|
58
|
-
// Not logging, because it's re-thrown upstream
|
|
59
|
-
fn.defer.reject(err);
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
.finally(() => {
|
|
63
|
-
this.inFlight--;
|
|
64
|
-
logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
65
|
-
// check if there's room to start next job
|
|
66
|
-
if (this.queue.length && this.inFlight <= concurrency) {
|
|
67
|
-
const nextFn = this.queue.shift();
|
|
68
|
-
void this.push(nextFn);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
if (this.inFlight === 0) {
|
|
72
|
-
logger.debug('onIdle');
|
|
73
|
-
this.onIdleListeners.forEach(defer => defer.resolve());
|
|
74
|
-
this.onIdleListeners.length = 0; // empty the array
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
35
|
+
this.start(job);
|
|
78
36
|
}
|
|
79
37
|
else {
|
|
80
|
-
this.queue.push(
|
|
81
|
-
logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`);
|
|
38
|
+
this.queue.push(job);
|
|
39
|
+
this.logger.debug(`inFlight ${this.inFlight}/${this.cfg.concurrency}, queue++ ${this.queue.length}`);
|
|
82
40
|
}
|
|
83
|
-
return await
|
|
41
|
+
return await job.defer;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Starts the job and settles its `defer`.
|
|
45
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
46
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
47
|
+
*/
|
|
48
|
+
start(job) {
|
|
49
|
+
const { concurrency } = this.cfg;
|
|
50
|
+
const { resolveOnStart, logger } = this;
|
|
51
|
+
const { defer } = job;
|
|
52
|
+
this.inFlight++;
|
|
53
|
+
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
54
|
+
if (resolveOnStart)
|
|
55
|
+
defer.resolve();
|
|
56
|
+
runSafe(job.fn)
|
|
57
|
+
.then(result => {
|
|
58
|
+
if (!resolveOnStart)
|
|
59
|
+
defer.resolve(result);
|
|
60
|
+
})
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
if (resolveOnStart) {
|
|
63
|
+
logger.error(err);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
67
|
+
logger.error(err);
|
|
68
|
+
defer.resolve(); // resolve with `void`
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Should be handled on the outside, otherwise it'll cause UnhandledRejection
|
|
72
|
+
// Not logging, because it's re-thrown upstream
|
|
73
|
+
defer.reject(err);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
this.inFlight--;
|
|
78
|
+
logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`);
|
|
79
|
+
// check if there's room to start next job
|
|
80
|
+
if (this.queue.length && this.inFlight < concurrency) {
|
|
81
|
+
this.start(this.queue.shift());
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (this.inFlight === 0) {
|
|
85
|
+
logger.debug('onIdle');
|
|
86
|
+
this.onIdleListeners.forEach(defer => defer.resolve());
|
|
87
|
+
this.onIdleListeners.length = 0; // empty the array
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
84
91
|
}
|
|
85
92
|
get queueSize() {
|
|
86
93
|
return this.queue.length;
|
package/dist/semver.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type SemverTokens = [major: number, minor: number, patch: number];
|
|
|
16
16
|
*
|
|
17
17
|
* _semver('1').toString() === '1.0.0'
|
|
18
18
|
*
|
|
19
|
+
* 4-part strings are treated as equal to 3-part strings, e.g.: '1.2.3.4' === '1.2.3'
|
|
20
|
+
*
|
|
19
21
|
* @experimental
|
|
20
22
|
*/
|
|
21
23
|
export declare class Semver {
|
package/dist/semver.js
CHANGED
package/package.json
CHANGED
|
@@ -1016,8 +1016,7 @@ class LocalTimeFactory {
|
|
|
1016
1016
|
*/
|
|
1017
1017
|
isTimezoneValid(tz: string): boolean {
|
|
1018
1018
|
if (tz === 'UTC') return true // we deliberately consider UTC a valid timezone, while it's mostly used in testing
|
|
1019
|
-
|
|
1020
|
-
return Intl.supportedValuesOf('timeZone').includes(tz)
|
|
1019
|
+
return Intl2.getSupportedTimezones().includes(tz as IANATimezone)
|
|
1021
1020
|
}
|
|
1022
1021
|
|
|
1023
1022
|
sort(items: LocalTime[], dir: SortDirection = 'asc', opt: MutateOptions = {}): LocalTime[] {
|
package/src/intl/intl.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _Memo } from '../decorators/memo.decorator.js'
|
|
2
|
-
import type { IANATimezone } from '../types.js'
|
|
2
|
+
import type { AnyObject, IANATimezone } from '../types.js'
|
|
3
3
|
|
|
4
4
|
// oxlint-disable no-restricted-globals
|
|
5
5
|
|
|
@@ -11,18 +11,37 @@ import type { IANATimezone } from '../types.js'
|
|
|
11
11
|
*
|
|
12
12
|
* Methods accept non-optional arguments consciously,
|
|
13
13
|
* to be able to cache them better. Just pass {} for options.
|
|
14
|
+
*
|
|
15
|
+
* The cache is never evicted, it grows with each unique combination of arguments.
|
|
16
|
+
* Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
|
|
14
17
|
*/
|
|
15
18
|
class MemoizedIntl {
|
|
16
19
|
/**
|
|
17
20
|
* Returns the IANA timezone e.g `Europe/Stockholm`.
|
|
18
21
|
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
|
22
|
+
*
|
|
23
|
+
* Memoized, so it returns the timezone at the time of the first call.
|
|
24
|
+
* Changing `process.env.TZ` at runtime after that is not reflected.
|
|
19
25
|
*/
|
|
20
26
|
@_Memo()
|
|
21
27
|
getTimezone(): IANATimezone {
|
|
22
28
|
return Intl.DateTimeFormat().resolvedOptions().timeZone as IANATimezone
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Returns the list of IANA timezones supported by the runtime (~420 entries).
|
|
33
|
+
* Memoized, because V8 rebuilds the list on every call (~70µs).
|
|
34
|
+
*
|
|
35
|
+
* The list contains canonical CLDR ids only, so valid aliases are missing from it,
|
|
36
|
+
* e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
|
|
37
|
+
* Don't use it to validate user input as is.
|
|
38
|
+
*/
|
|
25
39
|
@_Memo()
|
|
40
|
+
getSupportedTimezones(): readonly IANATimezone[] {
|
|
41
|
+
return Intl.supportedValuesOf('timeZone') as IANATimezone[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
26
45
|
DateTimeFormat(
|
|
27
46
|
locales: Intl.LocalesArgument,
|
|
28
47
|
options: Intl.DateTimeFormatOptions,
|
|
@@ -30,7 +49,7 @@ class MemoizedIntl {
|
|
|
30
49
|
return new Intl.DateTimeFormat(locales, options)
|
|
31
50
|
}
|
|
32
51
|
|
|
33
|
-
@_Memo()
|
|
52
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
34
53
|
RelativeTimeFormat(
|
|
35
54
|
locales: Intl.LocalesArgument,
|
|
36
55
|
options: Intl.RelativeTimeFormatOptions,
|
|
@@ -38,7 +57,7 @@ class MemoizedIntl {
|
|
|
38
57
|
return new Intl.RelativeTimeFormat(locales, options)
|
|
39
58
|
}
|
|
40
59
|
|
|
41
|
-
@_Memo()
|
|
60
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
42
61
|
DurationFormat(
|
|
43
62
|
locales: Intl.LocalesArgument,
|
|
44
63
|
options: Intl.DurationFormatOptions,
|
|
@@ -46,7 +65,7 @@ class MemoizedIntl {
|
|
|
46
65
|
return new Intl.DurationFormat(locales, options)
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
@_Memo()
|
|
68
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
50
69
|
NumberFormat(
|
|
51
70
|
locales: Intl.LocalesArgument,
|
|
52
71
|
options: Intl.NumberFormatOptions,
|
|
@@ -54,22 +73,22 @@ class MemoizedIntl {
|
|
|
54
73
|
return new Intl.NumberFormat(locales, options)
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
@_Memo()
|
|
76
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
58
77
|
Collator(locales: Intl.LocalesArgument, options: Intl.CollatorOptions): Intl.Collator {
|
|
59
78
|
return new Intl.Collator(locales, options)
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
@_Memo()
|
|
81
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
63
82
|
PluralRules(locales: Intl.LocalesArgument, options: Intl.PluralRulesOptions): Intl.PluralRules {
|
|
64
83
|
return new Intl.PluralRules(locales, options)
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
@_Memo()
|
|
86
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
68
87
|
ListFormat(locales: Intl.LocalesArgument, options: Intl.ListFormatOptions): Intl.ListFormat {
|
|
69
88
|
return new Intl.ListFormat(locales, options)
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
@_Memo()
|
|
91
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
73
92
|
DisplayNames(
|
|
74
93
|
locales: Intl.LocalesArgument,
|
|
75
94
|
options: Intl.DisplayNamesOptions,
|
|
@@ -77,10 +96,27 @@ class MemoizedIntl {
|
|
|
77
96
|
return new Intl.DisplayNames(locales, options)
|
|
78
97
|
}
|
|
79
98
|
|
|
80
|
-
@_Memo()
|
|
99
|
+
@_Memo({ cacheKeyFn: intlCacheKeyFn })
|
|
81
100
|
Segmenter(locales: Intl.LocalesArgument, options: Intl.SegmenterOptions): Intl.Segmenter {
|
|
82
101
|
return new Intl.Segmenter(locales, options)
|
|
83
102
|
}
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
export const Intl2 = new MemoizedIntl()
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Default `jsonMemoSerializer` would serialize `Intl.Locale` instances as `{}`,
|
|
109
|
+
* making all Locale arguments share one cache key. Convert them to their string tags instead.
|
|
110
|
+
*
|
|
111
|
+
* Options keys are serialized in sorted order, so that `{ a, b }` and `{ b, a }` share one cache key.
|
|
112
|
+
* Passing a keys array as replacer works, because Intl options are flat objects.
|
|
113
|
+
*/
|
|
114
|
+
function intlCacheKeyFn([locales, options]: [Intl.LocalesArgument, AnyObject]): string {
|
|
115
|
+
return JSON.stringify(localesKey(locales)) + JSON.stringify(options, Object.keys(options).sort())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function localesKey(locales: Intl.LocalesArgument): string | string[] | undefined {
|
|
119
|
+
if (locales === undefined) return
|
|
120
|
+
if (Array.isArray(locales)) return locales.map(String)
|
|
121
|
+
return String(locales)
|
|
122
|
+
}
|
|
@@ -63,7 +63,7 @@ export class PGradualQueue {
|
|
|
63
63
|
private warmupComplete: boolean
|
|
64
64
|
|
|
65
65
|
inFlight = 0
|
|
66
|
-
private queue:
|
|
66
|
+
private queue: PGradualQueueJob<any>[] = []
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Get current allowed concurrency based on warmup progress.
|
|
@@ -89,54 +89,62 @@ export class PGradualQueue {
|
|
|
89
89
|
* Push PromiseReturningFunction to the Queue.
|
|
90
90
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
91
91
|
*/
|
|
92
|
-
async push<R>(
|
|
92
|
+
async push<R>(fn: AsyncFunction<R>): Promise<R> {
|
|
93
93
|
// Initialize start time on first push
|
|
94
94
|
if (this.startTime === 0) {
|
|
95
95
|
this.startTime = Date.now()
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const {
|
|
99
|
-
const fn = fn_ as AsyncFunctionWithDefer<R>
|
|
100
|
-
fn.defer ||= pDefer<R>()
|
|
101
|
-
|
|
98
|
+
const job: PGradualQueueJob<R> = { fn, defer: pDefer<R>() }
|
|
102
99
|
const concurrency = this.getCurrentConcurrency()
|
|
103
100
|
|
|
104
101
|
if (this.inFlight < concurrency) {
|
|
105
|
-
this.
|
|
106
|
-
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
107
|
-
|
|
108
|
-
runSafe(fn)
|
|
109
|
-
.then(result => {
|
|
110
|
-
fn.defer.resolve(result)
|
|
111
|
-
})
|
|
112
|
-
.catch((err: Error) => {
|
|
113
|
-
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
114
|
-
logger.error(err)
|
|
115
|
-
fn.defer.resolve() // resolve with `void`
|
|
116
|
-
} else {
|
|
117
|
-
fn.defer.reject(err)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
.finally(() => {
|
|
121
|
-
this.inFlight--
|
|
122
|
-
const currentConcurrency = this.getCurrentConcurrency()
|
|
123
|
-
logger.debug(
|
|
124
|
-
`inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
// Start queued jobs up to the current concurrency limit
|
|
128
|
-
// Use while loop since concurrency may have increased during warmup
|
|
129
|
-
while (this.queue.length && this.inFlight < this.getCurrentConcurrency()) {
|
|
130
|
-
const nextFn = this.queue.shift()!
|
|
131
|
-
void this.push(nextFn)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
102
|
+
this.start(job, concurrency)
|
|
134
103
|
} else {
|
|
135
|
-
this.queue.push(
|
|
136
|
-
logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`)
|
|
104
|
+
this.queue.push(job)
|
|
105
|
+
this.logger.debug(`inFlight ${this.inFlight}/${concurrency}, queue++ ${this.queue.length}`)
|
|
137
106
|
}
|
|
138
107
|
|
|
139
|
-
return await
|
|
108
|
+
return await job.defer
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Starts the job and settles its `defer`.
|
|
113
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
114
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
115
|
+
*/
|
|
116
|
+
private start<R>(job: PGradualQueueJob<R>, concurrency: number): void {
|
|
117
|
+
const { logger } = this
|
|
118
|
+
const { defer } = job
|
|
119
|
+
|
|
120
|
+
this.inFlight++
|
|
121
|
+
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
122
|
+
|
|
123
|
+
runSafe(job.fn)
|
|
124
|
+
.then(result => {
|
|
125
|
+
defer.resolve(result)
|
|
126
|
+
})
|
|
127
|
+
.catch((err: Error) => {
|
|
128
|
+
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
129
|
+
logger.error(err)
|
|
130
|
+
defer.resolve() // resolve with `void`
|
|
131
|
+
} else {
|
|
132
|
+
defer.reject(err)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.finally(() => {
|
|
136
|
+
this.inFlight--
|
|
137
|
+
const currentConcurrency = this.getCurrentConcurrency()
|
|
138
|
+
logger.debug(
|
|
139
|
+
`inFlight-- ${this.inFlight}/${currentConcurrency}, queue ${this.queue.length}`,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Start queued jobs up to the current concurrency limit
|
|
143
|
+
// Use while loop since concurrency may have increased during warmup
|
|
144
|
+
while (this.queue.length && this.inFlight < currentConcurrency) {
|
|
145
|
+
this.start(this.queue.shift()!, currentConcurrency)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
get queueSize(): number {
|
|
@@ -164,6 +172,7 @@ function runSafe<R>(fn: AsyncFunction<R>): Promise<R> {
|
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
|
|
167
|
-
interface
|
|
175
|
+
interface PGradualQueueJob<R> {
|
|
176
|
+
fn: AsyncFunction<R>
|
|
168
177
|
defer: DeferredPromise<R>
|
|
169
178
|
}
|
package/src/promise/pQueue.ts
CHANGED
|
@@ -61,67 +61,77 @@ export class PQueue {
|
|
|
61
61
|
private readonly logger: CommonLogger
|
|
62
62
|
|
|
63
63
|
inFlight = 0
|
|
64
|
-
private queue:
|
|
64
|
+
private queue: PQueueJob<any>[] = []
|
|
65
65
|
private onIdleListeners: DeferredPromise[] = []
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Push PromiseReturningFunction to the Queue.
|
|
69
69
|
* Returns a Promise that resolves (or rejects) with the return value from the Promise.
|
|
70
70
|
*/
|
|
71
|
-
async push<R>(
|
|
72
|
-
const {
|
|
73
|
-
const { resolveOnStart, logger } = this
|
|
71
|
+
async push<R>(fn: AsyncFunction<R>): Promise<R> {
|
|
72
|
+
const job: PQueueJob<R> = { fn, defer: pDefer<R>() }
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
fn.defer ||= pDefer<R>()
|
|
77
|
-
|
|
78
|
-
if (this.inFlight < concurrency) {
|
|
74
|
+
if (this.inFlight < this.cfg.concurrency) {
|
|
79
75
|
// There is room for more jobs. Can start immediately
|
|
80
|
-
this.
|
|
81
|
-
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
82
|
-
if (resolveOnStart) fn.defer.resolve()
|
|
83
|
-
|
|
84
|
-
runSafe(fn)
|
|
85
|
-
.then(result => {
|
|
86
|
-
if (!resolveOnStart) fn.defer.resolve(result)
|
|
87
|
-
})
|
|
88
|
-
.catch((err: Error) => {
|
|
89
|
-
if (resolveOnStart) {
|
|
90
|
-
logger.error(err)
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
95
|
-
logger.error(err)
|
|
96
|
-
fn.defer.resolve() // resolve with `void`
|
|
97
|
-
} else {
|
|
98
|
-
// Should be handled on the outside, otherwise it'll cause UnhandledRejection
|
|
99
|
-
// Not logging, because it's re-thrown upstream
|
|
100
|
-
fn.defer.reject(err)
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
.finally(() => {
|
|
104
|
-
this.inFlight--
|
|
105
|
-
logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
106
|
-
|
|
107
|
-
// check if there's room to start next job
|
|
108
|
-
if (this.queue.length && this.inFlight <= concurrency) {
|
|
109
|
-
const nextFn = this.queue.shift()!
|
|
110
|
-
void this.push(nextFn)
|
|
111
|
-
} else {
|
|
112
|
-
if (this.inFlight === 0) {
|
|
113
|
-
logger.debug('onIdle')
|
|
114
|
-
this.onIdleListeners.forEach(defer => defer.resolve())
|
|
115
|
-
this.onIdleListeners.length = 0 // empty the array
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
})
|
|
76
|
+
this.start(job)
|
|
119
77
|
} else {
|
|
120
|
-
this.queue.push(
|
|
121
|
-
logger.debug(
|
|
78
|
+
this.queue.push(job)
|
|
79
|
+
this.logger.debug(
|
|
80
|
+
`inFlight ${this.inFlight}/${this.cfg.concurrency}, queue++ ${this.queue.length}`,
|
|
81
|
+
)
|
|
122
82
|
}
|
|
123
83
|
|
|
124
|
-
return await
|
|
84
|
+
return await job.defer
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Starts the job and settles its `defer`.
|
|
89
|
+
* Intentionally not async: it's called both from `push` and when a running job finishes,
|
|
90
|
+
* and must not create a Promise that nobody handles (which would cause UnhandledRejection).
|
|
91
|
+
*/
|
|
92
|
+
private start<R>(job: PQueueJob<R>): void {
|
|
93
|
+
const { concurrency } = this.cfg
|
|
94
|
+
const { resolveOnStart, logger } = this
|
|
95
|
+
const { defer } = job
|
|
96
|
+
|
|
97
|
+
this.inFlight++
|
|
98
|
+
logger.debug(`inFlight++ ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
99
|
+
if (resolveOnStart) defer.resolve()
|
|
100
|
+
|
|
101
|
+
runSafe(job.fn)
|
|
102
|
+
.then(result => {
|
|
103
|
+
if (!resolveOnStart) defer.resolve(result)
|
|
104
|
+
})
|
|
105
|
+
.catch((err: Error) => {
|
|
106
|
+
if (resolveOnStart) {
|
|
107
|
+
logger.error(err)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.cfg.errorMode === ErrorMode.SUPPRESS) {
|
|
112
|
+
logger.error(err)
|
|
113
|
+
defer.resolve() // resolve with `void`
|
|
114
|
+
} else {
|
|
115
|
+
// Should be handled on the outside, otherwise it'll cause UnhandledRejection
|
|
116
|
+
// Not logging, because it's re-thrown upstream
|
|
117
|
+
defer.reject(err)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.finally(() => {
|
|
121
|
+
this.inFlight--
|
|
122
|
+
logger.debug(`inFlight-- ${this.inFlight}/${concurrency}, queue ${this.queue.length}`)
|
|
123
|
+
|
|
124
|
+
// check if there's room to start next job
|
|
125
|
+
if (this.queue.length && this.inFlight < concurrency) {
|
|
126
|
+
this.start(this.queue.shift()!)
|
|
127
|
+
} else {
|
|
128
|
+
if (this.inFlight === 0) {
|
|
129
|
+
logger.debug('onIdle')
|
|
130
|
+
this.onIdleListeners.forEach(defer => defer.resolve())
|
|
131
|
+
this.onIdleListeners.length = 0 // empty the array
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
get queueSize(): number {
|
|
@@ -155,6 +165,7 @@ function runSafe<R>(fn: AsyncFunction<R>): Promise<R> {
|
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
interface
|
|
168
|
+
interface PQueueJob<R> {
|
|
169
|
+
fn: AsyncFunction<R>
|
|
159
170
|
defer: DeferredPromise<R>
|
|
160
171
|
}
|
package/src/semver.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type SemverTokens = [major: number, minor: number, patch: number]
|
|
|
20
20
|
*
|
|
21
21
|
* _semver('1').toString() === '1.0.0'
|
|
22
22
|
*
|
|
23
|
+
* 4-part strings are treated as equal to 3-part strings, e.g.: '1.2.3.4' === '1.2.3'
|
|
24
|
+
*
|
|
23
25
|
* @experimental
|
|
24
26
|
*/
|
|
25
27
|
export class Semver {
|