xcraft-core-utils 4.14.2 → 4.16.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.
@@ -4,11 +4,27 @@ const {throttle} = require('lodash');
4
4
  const watt = require('gigawatts');
5
5
 
6
6
  class ArrayCollector {
7
- constructor(resp, wait = 20, onCollect, leading = true) {
8
- this.onCollect = watt(onCollect);
7
+ constructor(resp, wait = 20, onCollect, leading = true, async = false) {
8
+ this._async = async;
9
+ if (!async) {
10
+ this.onCollect = watt(onCollect);
11
+ } else {
12
+ this.onCollect = onCollect;
13
+ }
9
14
  this.entries = {};
10
15
  this.resp = resp;
11
16
  this.release = throttle(this._release, wait, {leading});
17
+ this.releaseAsync = throttle(this._releaseAsync, wait, {leading});
18
+ }
19
+
20
+ async _releaseAsync() {
21
+ const copy = this.entries;
22
+ this.entries = {};
23
+ try {
24
+ await this.onCollect(copy, this.resp);
25
+ } catch (err) {
26
+ this.resp.log.err(err);
27
+ }
12
28
  }
13
29
 
14
30
  _release() {
@@ -26,11 +42,19 @@ class ArrayCollector {
26
42
 
27
43
  grab(key, data) {
28
44
  this._addByKey(key, data);
29
- this.release();
45
+ if (this._async) {
46
+ this.releaseAsync();
47
+ } else {
48
+ this.release();
49
+ }
30
50
  }
31
51
 
32
52
  cancel() {
33
- this.release.cancel();
53
+ if (this._async) {
54
+ this.releaseAsync.cancel();
55
+ } else {
56
+ this.release.cancel();
57
+ }
34
58
  }
35
59
  }
36
60
 
@@ -0,0 +1,227 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @param {string} dateOrDateTime
5
+ */
6
+ function toPlannerDate(dateOrDateTime) {
7
+ if (!dateOrDateTime) {
8
+ return dateOrDateTime;
9
+ }
10
+ if (dateOrDateTime.includes('T')) {
11
+ // datetime
12
+ return plainDateTimeISO(toJsDate(dateOrDateTime));
13
+ }
14
+ // date
15
+ return dateOrDateTime;
16
+ }
17
+
18
+ const zonedDateTimeRegex = /^(?<datetime>(?<date>-?([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))T(?<time>([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?|(24:00:00(\.0+)?)))(?<suffix>(?<offset>Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?(\[(?<timezone>!?[a-zA-Z0-9._+/-]+)\])?(?<tags>(\[(!?[a-z0-9_-]=[a-z0-9_-])\])*))$/;
19
+ const numOffsetRegex = /^(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)/;
20
+
21
+ /**
22
+ * @param {string} dateOrDateTime
23
+ * @returns {{datetime: string, date: string, time?: string, suffix?: string, offset?: string, timezone?: string, tags?: string}}
24
+ */
25
+ function parseZonedDateTime(dateOrDateTime) {
26
+ if (!dateOrDateTime.includes('T')) {
27
+ return {datetime: dateOrDateTime, date: dateOrDateTime};
28
+ }
29
+ const match = dateOrDateTime.match(zonedDateTimeRegex);
30
+ if (!match || !match.groups) {
31
+ throw new Error(`Bad date '${dateOrDateTime}'`);
32
+ }
33
+ return /**@type {any} */ (match.groups);
34
+ }
35
+
36
+ /**
37
+ * @param {{date: string, time?: string, offset?: string, timezone?: string, tags?: string}} parts
38
+ * @returns {string}
39
+ */
40
+ function zonedDateTimeFromParts(parts) {
41
+ const {date, time, offset = '', timezone = null, tags = ''} = parts;
42
+ if (!time) {
43
+ return date;
44
+ }
45
+ return `${date}T${time}${offset}${timezone ? `[${timezone}]` : ''}${tags}`;
46
+ }
47
+
48
+ /**
49
+ * @param {string} dateOrDateTime
50
+ * @param {boolean} [isEndDate=false]
51
+ */
52
+ function toJsDate(dateOrDateTime, isEndDate = false) {
53
+ if (!dateOrDateTime.includes('T')) {
54
+ const plainDate = dateOrDateTime;
55
+ if (isEndDate) {
56
+ return new Date(`${plainDate}T24:00:00`);
57
+ }
58
+ return new Date(`${plainDate}T00:00:00`);
59
+ }
60
+ const {datetime, offset = '', timezone} = parseZonedDateTime(dateOrDateTime);
61
+ if (offset === 'Z') {
62
+ return new Date(`${datetime}${offset}`);
63
+ }
64
+ if (timezone) {
65
+ return localDate(datetime, timezone);
66
+ }
67
+ return new Date(`${datetime}${offset}`);
68
+ }
69
+
70
+ /**
71
+ * @param {string} dateOrDateTime
72
+ * @param {boolean} [isEndDate=false]
73
+ */
74
+ function getTimestamp(dateOrDateTime, isEndDate = false) {
75
+ return toJsDate(dateOrDateTime, isEndDate).getTime();
76
+ }
77
+
78
+ /**
79
+ * @param {string} dateOrDateTime
80
+ * @returns {string | null}
81
+ */
82
+ function getTimezone(dateOrDateTime) {
83
+ const {offset, timezone} = parseZonedDateTime(dateOrDateTime);
84
+ if (timezone) {
85
+ return timezone;
86
+ }
87
+ if (!offset) {
88
+ return null;
89
+ }
90
+ return offset === 'Z' ? 'UTC' : offset;
91
+ }
92
+
93
+ /**
94
+ * @param {string} plainDateTime
95
+ * @param {string | null} timezone
96
+ * @returns {string}
97
+ */
98
+ function addTimezone(plainDateTime, timezone) {
99
+ if (!timezone) {
100
+ return plainDateTime;
101
+ }
102
+ if (timezone === 'UTC') {
103
+ return `${plainDateTime}Z`;
104
+ }
105
+ if (timezone.match(numOffsetRegex)) {
106
+ return `${plainDateTime}${timezone}`;
107
+ }
108
+ return `${plainDateTime}[${timezone}]`;
109
+ }
110
+
111
+ /**
112
+ * @param {string} dateTime
113
+ * @param {string | null} timezone
114
+ * @returns {string}
115
+ */
116
+ function setTimezone(dateTime, timezone) {
117
+ const {datetime} = parseZonedDateTime(dateTime);
118
+ return addTimezone(datetime, timezone);
119
+ }
120
+
121
+ /**
122
+ * @param {Date} date
123
+ * @param {string} timezone
124
+ * @returns {string}
125
+ */
126
+ function dateToTimezone(date, timezone) {
127
+ return date
128
+ .toLocaleString('sv', timezone ? {timeZone: timezone} : undefined)
129
+ .replace(' ', 'T');
130
+
131
+ // // Other version
132
+ // const {year, month, day, hour, minute, second} = Object.fromEntries(
133
+ // new Intl.DateTimeFormat('latin', {
134
+ // year: 'numeric',
135
+ // month: '2-digit',
136
+ // day: '2-digit',
137
+ // hour: '2-digit',
138
+ // minute: '2-digit',
139
+ // second: '2-digit',
140
+ // timeZone: timezone || undefined,
141
+ // })
142
+ // .formatToParts(date)
143
+ // .map(({type, value}) => [type, value])
144
+ // );
145
+ // return `${year}-${month}-${day}T${hour}${minute}${second}`
146
+ }
147
+
148
+ /**
149
+ * @param {Date} date
150
+ * @param {string | null} [timezone]
151
+ * @returns {string}
152
+ */
153
+ function dateToZonedDateTime(date, timezone) {
154
+ if (!timezone) {
155
+ return plainDateTimeISO(date);
156
+ }
157
+ const plainDateTime = dateToTimezone(date, timezone);
158
+ return addTimezone(plainDateTime, timezone);
159
+ }
160
+
161
+ /**
162
+ * @param {string} plainDateTime
163
+ * @param {string} timezone
164
+ * @returns {Date}
165
+ */
166
+ function localDate(plainDateTime, timezone) {
167
+ // Note: should be improved when the Temporal api will be available
168
+ const date = new Date(plainDateTime);
169
+ const dateTimestamp = date.getTime();
170
+ const zoneTimestamp = new Date(dateToTimezone(date, timezone)).getTime();
171
+ const timeDiff = dateTimestamp - zoneTimestamp;
172
+ return new Date(dateTimestamp + timeDiff);
173
+ }
174
+
175
+ /**
176
+ * @param {Date} date
177
+ */
178
+ function plainDateISO(date = new Date()) {
179
+ const year = date.getFullYear();
180
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
181
+ const day = date.getDate().toString().padStart(2, '0');
182
+ return `${year}-${month}-${day}`;
183
+ }
184
+
185
+ /**
186
+ * @param {Date} date
187
+ */
188
+ function plainTimeISO(date = new Date()) {
189
+ const hours = date.getHours().toString().padStart(2, '0');
190
+ const minutes = date.getMinutes().toString().padStart(2, '0');
191
+ const seconds = date.getSeconds().toString().padStart(2, '0');
192
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
193
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
194
+ }
195
+
196
+ /**
197
+ * @param {Date} date
198
+ */
199
+ function plainDateTimeISO(date = new Date()) {
200
+ const dateISO = plainDateISO(date);
201
+ const timeISO = plainTimeISO(date);
202
+ return `${dateISO}T${timeISO}`;
203
+ }
204
+
205
+ function nowZonedDateTimeISO() {
206
+ const plainDateTime = plainDateTimeISO();
207
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
208
+ return addTimezone(plainDateTime, timezone);
209
+ }
210
+
211
+ module.exports = {
212
+ toPlannerDate,
213
+ parseZonedDateTime,
214
+ zonedDateTimeFromParts,
215
+ toJsDate,
216
+ getTimestamp,
217
+ getTimezone,
218
+ addTimezone,
219
+ setTimezone,
220
+ dateToZonedDateTime,
221
+ dateToTimezone,
222
+ localDate,
223
+ plainDateISO,
224
+ plainTimeISO,
225
+ plainDateTimeISO,
226
+ nowZonedDateTimeISO,
227
+ };
package/lib/locks.js CHANGED
@@ -31,6 +31,10 @@ class Mutex {
31
31
  unlock() {
32
32
  this._mutex.unlock();
33
33
  }
34
+
35
+ get isLocked() {
36
+ return this._mutex.isLocked;
37
+ }
34
38
  }
35
39
 
36
40
  class RecursiveMutex extends Mutex {
package/lib/modules.js CHANGED
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const fse = require('fs-extra');
5
5
  const xFs = require('xcraft-core-fs');
6
6
  const _ = require('lodash');
7
+ const traverse = require('xcraft-traverse');
7
8
 
8
9
  function merge(obj, overloads) {
9
10
  _.mergeWith(obj, overloads, (_, src) =>
@@ -20,7 +21,26 @@ function applyOverloads(appDir, appId, variantId, app) {
20
21
  const overloads = JSON.parse(
21
22
  fse.readFileSync(path.join(appDir, appId, `app.${variantId}.json`))
22
23
  );
24
+
23
25
  merge(app.xcraft, overloads);
26
+
27
+ /* Case where a config.js file is generated for an app; (see xcraft-core-etc).
28
+ * This code removes the entry with '-0' in order to have fallback on the
29
+ * default values in the config.js files of each module.
30
+ */
31
+ const tr = traverse(app.xcraft);
32
+ tr.forEach(function (x) {
33
+ /* The -0 number is used in order to ensure that the config uses the default
34
+ * value provided in the module's config.js file. The -0 value is interesting
35
+ * because it's mostly useless and it can be used with JSON. In Javascript,
36
+ * 0 === -0 is true. In order to detect -0, the trick is to compare for
37
+ * Infinity because 1/0 !== 1/-0
38
+ * -- See xcraft-core-etc
39
+ */
40
+ if (x === 0 && 1 / 0 !== 1 / x) {
41
+ this.remove();
42
+ }
43
+ });
24
44
  } catch (ex) {
25
45
  if (ex.code !== 'ENOENT') {
26
46
  throw ex;
@@ -70,7 +90,7 @@ exports.loadAppConfig = (appId, appDir, configJson = {}, variantId = null) => {
70
90
  return;
71
91
  }
72
92
 
73
- configJson[appId] = exports.extractForEtc(appDir, appId, variantId);
93
+ configJson[appId] = exports.extractForEtc(appDir, appId, variantId, true);
74
94
  const hordesCfg = configJson[appId]['xcraft-core-horde'];
75
95
 
76
96
  if (hordesCfg && hordesCfg.hordes) {
package/lib/rest.js CHANGED
@@ -24,7 +24,26 @@ class RestAPI {
24
24
 
25
25
  #buildOptions(options) {
26
26
  return {
27
+ retry: {
28
+ limit: 2,
29
+ statusCodes: [429],
30
+ },
27
31
  hooks: {
32
+ afterResponse: [
33
+ async (response, retryWithMergedOptions) => {
34
+ if (response.statusCode === 429) {
35
+ const retryAfter = response.headers['retry-after'];
36
+
37
+ if (retryAfter) {
38
+ const waitTime = parseInt(retryAfter, 10) * 1000;
39
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
40
+ return retryWithMergedOptions({});
41
+ }
42
+ }
43
+
44
+ return response;
45
+ },
46
+ ],
28
47
  beforeError: [
29
48
  (error) => {
30
49
  const {response} = error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xcraft-core-utils",
3
- "version": "4.14.2",
3
+ "version": "4.16.0",
4
4
  "description": "Xcraft utils",
5
5
  "main": "index.js",
6
6
  "engines": {