@nxtedition/lib 19.1.3 → 19.1.6

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 (4) hide show
  1. package/errors.js +14 -4
  2. package/package.json +1 -2
  3. package/s3.js +23 -8
  4. package/serializers.js +105 -36
package/errors.js CHANGED
@@ -42,12 +42,16 @@ export function parseError(error) {
42
42
  const kSeen = Symbol('kSeen')
43
43
 
44
44
  export function serializeError(error) {
45
+ return _serializeError(error, { depth: 0 })
46
+ }
47
+
48
+ function _serializeError(error, { depth }) {
45
49
  if (!error) {
46
50
  return null
47
51
  }
48
52
 
49
53
  if (typeof error === 'string') {
50
- return [serializeError({ message: error })]
54
+ return [_serializeError({ message: error }, { depth })]
51
55
  }
52
56
 
53
57
  if (Buffer.isBuffer(error)) {
@@ -55,13 +59,17 @@ export function serializeError(error) {
55
59
  }
56
60
 
57
61
  if (Array.isArray(error)) {
58
- return error.map(serializeError).filter(Boolean)
62
+ return error.map((x) => _serializeError(x, { depth })).filter(Boolean)
59
63
  }
60
64
 
61
65
  if (Object.prototype.hasOwnProperty.call(error, kSeen)) {
62
66
  return null
63
67
  }
64
68
 
69
+ if (depth > 8) {
70
+ return [{ messages: 'Maximum error serialization depth exceeded' }]
71
+ }
72
+
65
73
  error[kSeen] = undefined
66
74
 
67
75
  const type =
@@ -104,8 +112,10 @@ export function serializeError(error) {
104
112
  signalCode = SIGNALS[signalCode] ?? signalCode
105
113
  }
106
114
 
107
- errors = Array.isArray(errors) ? errors.map(serializeError).filter(Boolean) : undefined
108
- cause = cause ? serializeError(cause) : undefined
115
+ errors = Array.isArray(errors)
116
+ ? errors.map((x) => _serializeError(x, { depth: depth + 1 })).filter(Boolean)
117
+ : undefined
118
+ cause = cause ? _serializeError(cause, { depth: depth + 1 }) : undefined
109
119
 
110
120
  delete error[kSeen]
111
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "19.1.3",
3
+ "version": "19.1.6",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -94,7 +94,6 @@
94
94
  "object-hash": "^3.0.0",
95
95
  "p-queue": "^8.0.1",
96
96
  "pino": "^8.20.0",
97
- "pino-std-serializers": "^6.2.2",
98
97
  "qs": "^6.12.1",
99
98
  "request-target": "^1.0.2",
100
99
  "smpte-timecode": "^1.3.5",
package/s3.js CHANGED
@@ -4,7 +4,8 @@ import tp from 'node:timers/promises'
4
4
  import AWS from '@aws-sdk/client-s3'
5
5
  import PQueue from 'p-queue'
6
6
 
7
- const queue = new PQueue({ concurrency: 8 })
7
+ const QUEUE = new PQueue({ concurrency: 8 })
8
+ const MD5_HEX_EXPR = /^[A-F0-9]{32}$/i
8
9
 
9
10
  /**
10
11
  * Uploads a file to S3 using multipart upload.
@@ -14,6 +15,7 @@ const queue = new PQueue({ concurrency: 8 })
14
15
  * @param {AbortSignal} options.signal - The signal to abort the upload.
15
16
  * @param {Object} options.logger - The logger to use.
16
17
  * @param {number} [options.partSize=16e6] - The size of each part in the multipart upload.
18
+ * @param {PQueue} [options.queue] - The queue to use for part uploads.
17
19
  * @param {Object} options.params - The parameters for the upload.
18
20
  * @param {Buffer|NodeJS.ReadStream} options.params.Body - The data to upload.
19
21
  * @param {string} options.params.Key - The key of the object.
@@ -22,7 +24,14 @@ const queue = new PQueue({ concurrency: 8 })
22
24
  * @param {number} [options.params.ContentLength] - The length of the object.
23
25
  * @returns {Promise<Object>} The result of the upload.
24
26
  */
25
- export async function upload({ client: s3, signal, logger, partSize = 16e6, params }) {
27
+ export async function upload({
28
+ client: s3,
29
+ queue = QUEUE,
30
+ signal,
31
+ logger,
32
+ partSize = 16e6,
33
+ params,
34
+ }) {
26
35
  if (s3 == null) {
27
36
  throw new Error('Invalid client')
28
37
  }
@@ -35,9 +44,18 @@ export async function upload({ client: s3, signal, logger, partSize = 16e6, para
35
44
  throw new Error('Invalid params')
36
45
  }
37
46
 
38
- const { Body, Key, Bucket, ContentMD5, ContentLength } = params ?? {}
47
+ const { Body, Key, Bucket, ContentMD5, ContentLength } = params
39
48
 
40
- // TODO (fix): Valdate ContentMD & ContentLength
49
+ const size = ContentLength != null ? Number(ContentLength) : null
50
+
51
+ if (size != null && (!Number.isFinite(size) || size < 0)) {
52
+ throw new Error('Invalid params.ContentLength')
53
+ }
54
+
55
+ const hash = ContentMD5 != null ? Buffer.from(ContentMD5, 'base64').toString('hex') : null
56
+ if (hash != null && !MD5_HEX_EXPR.test(hash)) {
57
+ throw new Error('Invalid params.ContentMD5')
58
+ }
41
59
 
42
60
  const promises = []
43
61
 
@@ -123,7 +141,7 @@ export async function upload({ client: s3, signal, logger, partSize = 16e6, para
123
141
  } catch (err) {
124
142
  logger?.warn({ err }, 'part upload failed')
125
143
 
126
- if (retryCount < 3) {
144
+ if (retryCount < 5) {
127
145
  logger?.debug({ retryCount }, 'part upload retry')
128
146
  await tp.setTimeout(1e3, undefined, { signal: uploader.signal })
129
147
  } else {
@@ -196,9 +214,6 @@ export async function upload({ client: s3, signal, logger, partSize = 16e6, para
196
214
  parts,
197
215
  }
198
216
 
199
- const size = ContentLength != null ? Number(ContentLength) : null
200
- const hash = ContentMD5 ? Buffer.from(ContentMD5, 'base64').toString('hex') : null
201
-
202
217
  if (size != null && size !== result.size) {
203
218
  throw new Error(`Expected size ${size} but got ${result.size}`)
204
219
  }
package/serializers.js CHANGED
@@ -1,4 +1,3 @@
1
- import serializers from 'pino-std-serializers'
2
1
  import { SIGNALS } from './platform.js'
3
2
  import { util } from 'undici'
4
3
 
@@ -25,41 +24,7 @@ function getHeaders(obj) {
25
24
  export default {
26
25
  data: (data) =>
27
26
  data != null && typeof data === 'object' ? JSON.stringify(data, undefined, 2) : data,
28
- err: (err) => {
29
- // TODO (fix): Merge with errors/serializeError?
30
-
31
- if (Buffer.isBuffer(err)) {
32
- err = new Error('unexpected buffer error')
33
- }
34
-
35
- if (Array.isArray(err)) {
36
- err = err.length === 1 ? err[0] : new AggregateError(err)
37
- }
38
-
39
- if (err == null) {
40
- return undefined
41
- }
42
-
43
- const ret = serializers.err(err)
44
-
45
- if (ret == null) {
46
- return undefined
47
- }
48
-
49
- if (ret.data !== null && typeof ret.data === 'object') {
50
- ret.data = JSON.stringify(ret.data)
51
- }
52
-
53
- if (typeof ret.signal === 'number') {
54
- ret.signal = SIGNALS[ret.signal] ?? String(ret.signal)
55
- }
56
-
57
- if (typeof ret.code === 'number') {
58
- ret.code = String(ret.code)
59
- }
60
-
61
- return ret
62
- },
27
+ err: (err) => errSerializer(err),
63
28
  res: (res) =>
64
29
  res && {
65
30
  id: res.id || res.req?.id || getHeader(res, 'request-id') || getHeader(res.req, 'request-id'),
@@ -124,3 +89,107 @@ export default {
124
89
  }
125
90
  },
126
91
  }
92
+
93
+ // Based on: https://github.com/pinojs/pino-std-serializers
94
+
95
+ const seen = Symbol('circular-ref-tag')
96
+ const rawSymbol = Symbol('pino-raw-err-ref')
97
+
98
+ const pinoErrProto = Object.create(
99
+ {},
100
+ {
101
+ type: {
102
+ enumerable: true,
103
+ writable: true,
104
+ value: undefined,
105
+ },
106
+ message: {
107
+ enumerable: true,
108
+ writable: true,
109
+ value: undefined,
110
+ },
111
+ stack: {
112
+ enumerable: true,
113
+ writable: true,
114
+ value: undefined,
115
+ },
116
+ aggregateErrors: {
117
+ enumerable: true,
118
+ writable: true,
119
+ value: undefined,
120
+ },
121
+ raw: {
122
+ enumerable: false,
123
+ get: function () {
124
+ return this[rawSymbol]
125
+ },
126
+ set: function (val) {
127
+ this[rawSymbol] = val
128
+ },
129
+ },
130
+ },
131
+ )
132
+ Object.defineProperty(pinoErrProto, rawSymbol, {
133
+ writable: true,
134
+ value: {},
135
+ })
136
+
137
+ function errSerializer(err) {
138
+ if (Array.isArray(err)) {
139
+ if (err.length === 0) {
140
+ return null
141
+ } else if (err.length === 1) {
142
+ return errSerializer(err[0])
143
+ } else {
144
+ return new AggregateError(err.map(errSerializer))
145
+ }
146
+ }
147
+
148
+ if (!isErrorLike(err)) {
149
+ return null
150
+ }
151
+
152
+ err[seen] = undefined // tag to prevent re-looking at this
153
+ const _err = Object.create(pinoErrProto)
154
+ _err.type =
155
+ toString.call(err.constructor) === '[object Function]' ? err.constructor.name : err.name
156
+ _err.message = err.message
157
+ _err.stack = err.stack
158
+
159
+ if (Array.isArray(err.errors)) {
160
+ _err.aggregateErrors = err.errors.map((err) => errSerializer(err))
161
+ }
162
+
163
+ if (isErrorLike(err.cause) && !Object.prototype.hasOwnProperty.call(err.cause, seen)) {
164
+ _err.cause = errSerializer(err.cause)
165
+ }
166
+
167
+ for (const key in err) {
168
+ if (_err[key] === undefined) {
169
+ const val = err[key]
170
+ if (isErrorLike(val)) {
171
+ if (!Object.prototype.hasOwnProperty.call(val, seen)) {
172
+ _err[key] = errSerializer(val)
173
+ }
174
+ } else if (val == null) {
175
+ // Do nothing...
176
+ } else if (key === 'data' && typeof val === 'object') {
177
+ _err[key] = JSON.stringify(val, undefined, 2)
178
+ } else if (key === 'code' && typeof val === 'number') {
179
+ _err[key] = String(val)
180
+ } else if (key === 'signal' && typeof val === 'number') {
181
+ _err[key] = SIGNALS[val] ?? String(val)
182
+ } else {
183
+ _err[key] = val
184
+ }
185
+ }
186
+ }
187
+
188
+ delete err[seen] // clean up tag in case err is serialized again later
189
+ _err.raw = err
190
+ return _err
191
+ }
192
+
193
+ const isErrorLike = (err) => {
194
+ return err && typeof err.message === 'string'
195
+ }