@nxtedition/lib 19.1.1 → 19.1.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.
Files changed (3) hide show
  1. package/couch.js +111 -60
  2. package/package.json +2 -1
  3. package/s3.js +18 -11
package/couch.js CHANGED
@@ -6,9 +6,10 @@ import { makeWeakCache } from './weakCache.js'
6
6
  import tp from 'timers/promises'
7
7
  import { defaultDelay as delay } from './http.js'
8
8
  import urljoin from 'url-join'
9
- import undici from 'undici'
9
+ import undici, { util as undiciUtil } from 'undici'
10
10
  import { AbortError } from './errors.js'
11
11
  import split2 from 'split2'
12
+ import { parse as parseContentType } from 'content-type'
12
13
 
13
14
  // https://github.com/fastify/fastify/blob/main/lib/reqIdGenFactory.js
14
15
  // 2,147,483,647 (2^31 − 1) stands for max SMI value (an internal optimization of V8).
@@ -886,22 +887,15 @@ function _normalizeAllDocsOptions({
886
887
  }
887
888
  }
888
889
 
889
- class Handler extends undici.DecoratorHandler {
890
- #str
891
- #decoder
892
- #onAbort
890
+ class SignalHandler extends undici.DecoratorHandler {
893
891
  #signal
894
- #statusCode
895
- #statusText
896
- #opts
897
892
  #handler
893
+ #abort
898
894
 
899
- constructor({ opts, signal, handler }) {
895
+ constructor({ signal, handler }) {
900
896
  super(handler)
901
-
902
- this.#handler = handler
903
- this.#opts = opts
904
897
  this.#signal = signal
898
+ this.#handler = handler
905
899
  }
906
900
 
907
901
  onConnect(abort) {
@@ -909,50 +903,94 @@ class Handler extends undici.DecoratorHandler {
909
903
  abort(this.#signal.reason)
910
904
  } else {
911
905
  if (this.#signal) {
912
- this.#onAbort = () => abort(this.#signal.reason)
913
- this.#signal.addEventListener('abort', this.#onAbort)
906
+ this.#abort = () => abort(this.#signal.reason)
907
+ this.#signal.addEventListener('abort', this.#abort)
914
908
  }
915
-
916
- this.#str = ''
917
- this.#decoder = null
918
909
  this.#handler.onConnect?.(abort)
919
910
  }
920
911
  }
921
912
 
922
- onHeaders(statusCode, rawHeaders, resume, statusText) {
913
+ onComplete(...args) {
914
+ this.#signal?.removeEventListener('abort', this.#abort)
915
+ this.#handler.onComplete?.(...args)
916
+ }
917
+
918
+ onError(...args) {
919
+ this.#signal?.removeEventListener('abort', this.#abort)
920
+ this.#handler.onError?.(...args)
921
+ }
922
+ }
923
+
924
+ class ErrorHandler extends undici.DecoratorHandler {
925
+ #str
926
+ #decoder
927
+ #statusCode
928
+ #statusText
929
+ #contentType
930
+ #opts
931
+ #handler
932
+ #error
933
+ #headers
934
+
935
+ constructor({ opts, handler }) {
936
+ super(handler)
937
+
938
+ this.#handler = handler
939
+ this.#opts = opts
940
+ }
941
+
942
+ onConnect(...args) {
943
+ this.#str = ''
944
+ this.#decoder = null
945
+ this.#handler.onConnect?.(...args)
946
+ }
947
+
948
+ onHeaders(statusCode, headers, resume, statusText) {
923
949
  this.#statusCode = statusCode
924
950
  this.#statusText = statusText
925
951
 
926
- if (this.#statusCode >= 200 && this.#statusCode < 300) {
927
- this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusText)
928
- } else {
952
+ if (this.#statusCode < 200 || this.#statusCode > 300) {
953
+ this.#headers = Array.isArray(headers) ? undiciUtil.parseHeaders(headers) : headers
954
+
955
+ this.#error = new Error(this.#statusText ?? this.#statusCode)
956
+
957
+ this.#contentType = parseContentType(this.#headers['content-type'] ?? '')?.type
958
+ if (this.#contentType !== 'application/json' && this.#contentType !== 'plain/text') {
959
+ throw this.#error
960
+ }
961
+
929
962
  this.#decoder = new TextDecoder()
963
+ } else {
964
+ return this.#handler.onHeaders?.(statusCode, headers, resume, statusText)
930
965
  }
931
966
  }
932
967
 
933
968
  onData(data) {
934
- if (this.#decoder) {
935
- this.#str += this.#decoder.decode(data, { stream: true })
969
+ if (this.#error) {
970
+ this.#str += this.#decoder?.decode(data, { stream: true }) ?? ''
936
971
  } else {
937
972
  return this.#handler?.onData(data)
938
973
  }
939
974
  }
940
975
 
941
- onComplete(trailers) {
942
- if (this.#decoder) {
943
- let body
944
- let reason
945
- let error
946
- try {
947
- body = JSON.parse(this.#str)
948
- reason = body.reason
949
- error = body.error
950
- } catch {
951
- // Do nothing...
952
- }
976
+ onComplete(...args) {
977
+ if (this.#error) {
978
+ this.#str += this.#decoder?.decode(undefined, { stream: false }) ?? ''
979
+
980
+ if (this.#contentType === 'application/json') {
981
+ let body
982
+ let reason
983
+ let error
984
+ try {
985
+ body = JSON.parse(this.#str)
986
+ reason = body.reason
987
+ error = body.error
988
+ } catch {
989
+ body = this.#str
990
+ // Do nothing...
991
+ }
953
992
 
954
- this.onFinally(
955
- Object.assign(new Error(this.#statusText ?? reason), {
993
+ throw Object.assign(this.#error, {
956
994
  reason,
957
995
  error,
958
996
  data: {
@@ -962,26 +1000,23 @@ class Handler extends undici.DecoratorHandler {
962
1000
  statusCode: this.#statusCode,
963
1001
  },
964
1002
  },
965
- }),
966
- )
967
- } else {
968
- this.onFinally(null, trailers)
969
- }
970
- }
971
-
972
- onError(err) {
973
- this.onFinally(err)
974
- }
975
-
976
- onFinally(err, val) {
977
- if (this.#signal) {
978
- this.#signal.removeEventListener('abort', this.#onAbort)
979
- }
1003
+ })
1004
+ } else if (this.#contentType === 'plain/text') {
1005
+ throw Object.assign(this.#error, {
1006
+ data: {
1007
+ ureq: this.#opts,
1008
+ ures: {
1009
+ body: this.#str,
1010
+ statusCode: this.#statusCode,
1011
+ headers: this.#headers,
1012
+ },
1013
+ },
1014
+ })
1015
+ }
980
1016
 
981
- if (err) {
982
- this.#handler.onError?.(err)
1017
+ assert(false)
983
1018
  } else {
984
- this.#handler.onComplete?.(val)
1019
+ this.#handler.onComplete?.(...args)
985
1020
  }
986
1021
  }
987
1022
  }
@@ -992,10 +1027,12 @@ class StreamOutput extends stream.Readable {
992
1027
  #abort
993
1028
  #state
994
1029
  #decoder
1030
+ #batched
995
1031
  #didPush = false
996
1032
 
997
- constructor({ highWaterMark }) {
1033
+ constructor({ batched = false, highWaterMark = batched ? 2 : 128 }) {
998
1034
  super({ objectMode: true, highWaterMark })
1035
+ this.#batched = batched
999
1036
  }
1000
1037
 
1001
1038
  _read() {
@@ -1033,6 +1070,7 @@ class StreamOutput extends stream.Readable {
1033
1070
  lines[0] = this.#str + lines[0]
1034
1071
  this.#str = lines.pop() ?? ''
1035
1072
 
1073
+ const rows = this.#batched ? [] : null
1036
1074
  for (const line of lines) {
1037
1075
  if (this.#state === 0) {
1038
1076
  if (line.endsWith('[')) {
@@ -1044,12 +1082,22 @@ class StreamOutput extends stream.Readable {
1044
1082
  if (line.startsWith(']')) {
1045
1083
  this.#state = 2
1046
1084
  } else {
1047
- this.push(JSON.parse(line.slice(0, line.lastIndexOf('}') + 1)))
1048
- this.#didPush = true
1085
+ const row = JSON.parse(line.slice(0, line.lastIndexOf('}') + 1))
1086
+ if (rows) {
1087
+ rows.push(row)
1088
+ } else {
1089
+ this.push(row)
1090
+ this.#didPush = true
1091
+ }
1049
1092
  }
1050
1093
  }
1051
1094
  }
1052
1095
 
1096
+ if (rows) {
1097
+ this.push(rows)
1098
+ this.#didPush = true
1099
+ }
1100
+
1053
1101
  return this.readableLength < this.readableHighWaterMark
1054
1102
  }
1055
1103
 
@@ -1121,6 +1169,8 @@ export function request(
1121
1169
  method,
1122
1170
  headers: {
1123
1171
  'content-type': body != null && typeof body === 'object' ? 'application/json' : 'plain/text',
1172
+ 'user-agent': globalThis.userAgent,
1173
+ 'request-id': genReqId(),
1124
1174
  accept: 'application/json',
1125
1175
  ...headers,
1126
1176
  },
@@ -1128,11 +1178,12 @@ export function request(
1128
1178
  }
1129
1179
 
1130
1180
  dispatcher = dispatcher.compose(
1131
- (dispatch) => (opts, handler) => dispatch(opts, new Handler({ opts, signal, handler })),
1181
+ (dispatch) => (opts, handler) => dispatch(opts, new ErrorHandler({ opts, handler })),
1182
+ (dispatch) => (opts, handler) => dispatch(opts, new SignalHandler({ signal, handler })),
1132
1183
  )
1133
1184
 
1134
1185
  if (stream) {
1135
- const handler = new StreamOutput({ highWaterMark: stream.highWaterMark ?? 128 })
1186
+ const handler = new StreamOutput(stream)
1136
1187
  dispatcher.dispatch(opts, handler)
1137
1188
  return handler
1138
1189
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "19.1.1",
3
+ "version": "19.1.3",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -79,6 +79,7 @@
79
79
  "@elastic/elasticsearch": "^8.13.1",
80
80
  "@elastic/transport": "^8.5.1",
81
81
  "@nxtedition/nxt-undici": "^2.0.46",
82
+ "content-type": "^1.0.5",
82
83
  "date-fns": "^3.6.0",
83
84
  "fast-querystring": "^1.1.1",
84
85
  "hasha": "^6.0.0",
package/s3.js CHANGED
@@ -4,11 +4,24 @@ 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 CONTENT_MD5_EXPR = /^[A-F0-9]{32}$/i
8
- const CONTENT_LENGTH_EXPR = /^\d+$/i
9
-
10
7
  const queue = new PQueue({ concurrency: 8 })
11
8
 
9
+ /**
10
+ * Uploads a file to S3 using multipart upload.
11
+ *
12
+ * @param {Object} options - The options for the upload.
13
+ * @param {AWS.S3} options.client - The S3 client.
14
+ * @param {AbortSignal} options.signal - The signal to abort the upload.
15
+ * @param {Object} options.logger - The logger to use.
16
+ * @param {number} [options.partSize=16e6] - The size of each part in the multipart upload.
17
+ * @param {Object} options.params - The parameters for the upload.
18
+ * @param {Buffer|NodeJS.ReadStream} options.params.Body - The data to upload.
19
+ * @param {string} options.params.Key - The key of the object.
20
+ * @param {string} options.params.Bucket - The name of the bucket.
21
+ * @param {string} [options.params.ContentMD5] - The MD5 hash of the object as base64 string.
22
+ * @param {number} [options.params.ContentLength] - The length of the object.
23
+ * @returns {Promise<Object>} The result of the upload.
24
+ */
12
25
  export async function upload({ client: s3, signal, logger, partSize = 16e6, params }) {
13
26
  if (s3 == null) {
14
27
  throw new Error('Invalid client')
@@ -24,13 +37,7 @@ export async function upload({ client: s3, signal, logger, partSize = 16e6, para
24
37
 
25
38
  const { Body, Key, Bucket, ContentMD5, ContentLength } = params ?? {}
26
39
 
27
- if (ContentMD5 != null && !CONTENT_MD5_EXPR.test(ContentMD5)) {
28
- throw new Error(`Invalid ContentMD5: ${ContentMD5}`)
29
- }
30
-
31
- if (ContentLength != null && !CONTENT_LENGTH_EXPR.test(ContentLength)) {
32
- throw new Error(`Invalid ContentLength: ${ContentLength}`)
33
- }
40
+ // TODO (fix): Valdate ContentMD & ContentLength
34
41
 
35
42
  const promises = []
36
43
 
@@ -190,7 +197,7 @@ export async function upload({ client: s3, signal, logger, partSize = 16e6, para
190
197
  }
191
198
 
192
199
  const size = ContentLength != null ? Number(ContentLength) : null
193
- const hash = ContentMD5
200
+ const hash = ContentMD5 ? Buffer.from(ContentMD5, 'base64').toString('hex') : null
194
201
 
195
202
  if (size != null && size !== result.size) {
196
203
  throw new Error(`Expected size ${size} but got ${result.size}`)