@nxtedition/lib 19.1.0 → 19.1.2

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 (2) hide show
  1. package/couch.js +229 -97
  2. package/package.json +8 -7
package/couch.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import assert from 'node:assert'
2
+ import stream from 'node:stream'
2
3
  import querystring from 'node:querystring'
3
4
  import createError from 'http-errors'
4
5
  import { makeWeakCache } from './weakCache.js'
5
6
  import tp from 'timers/promises'
6
7
  import { defaultDelay as delay } from './http.js'
7
8
  import urljoin from 'url-join'
8
- import undici from 'undici'
9
+ import undici, { util as undiciUtil } from 'undici'
9
10
  import { AbortError } from './errors.js'
10
11
  import split2 from 'split2'
11
- import stream from 'node:stream'
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,101 +887,96 @@ function _normalizeAllDocsOptions({
886
887
  }
887
888
  }
888
889
 
889
- class Handler {
890
- #state = 0
891
- #str = ''
892
- #decoder = new TextDecoder()
893
- #resolve
894
- #reject
895
- #resume
896
- #abort
897
- #onAbort
890
+ class SignalHandler extends undici.DecoratorHandler {
898
891
  #signal
899
- #next
900
- #statusCode
901
- #statusText
902
- #opts
892
+ #handler
893
+ #abort
903
894
 
904
- constructor({ opts, signal, resolve, reject, next }) {
905
- this.#next = next
906
- this.#opts = opts
907
- this.#resolve = resolve
908
- this.#reject = reject
895
+ constructor({ signal, handler }) {
896
+ super(handler)
909
897
  this.#signal = signal
898
+ this.#handler = handler
910
899
  }
911
900
 
912
901
  onConnect(abort) {
913
902
  if (this.#signal?.aborted) {
914
903
  abort(this.#signal.reason)
915
904
  } else {
916
- this.#abort = abort
917
905
  if (this.#signal) {
918
- this.#onAbort = () => abort(this.#signal.reason)
919
- this.#signal.addEventListener('abort', this.#onAbort)
906
+ this.#abort = () => abort(this.#signal.reason)
907
+ this.#signal.addEventListener('abort', this.#abort)
920
908
  }
909
+ this.#handler.onConnect?.(abort)
921
910
  }
922
911
  }
923
912
 
924
- onHeaders(statusCode, rawHeaders, resume, statusText) {
925
- this.#statusCode = statusCode
926
- this.#statusText = statusText
927
- this.#resume = resume
913
+ onComplete(...args) {
914
+ this.#signal?.removeEventListener('abort', this.#abort)
915
+ this.#handler.onComplete?.(...args)
916
+ }
928
917
 
929
- if (this.#statusCode >= 200 && this.#statusCode < 300) {
930
- this.#next = null
931
- }
918
+ onError(...args) {
919
+ this.#signal?.removeEventListener('abort', this.#abort)
920
+ this.#handler.onError?.(...args)
932
921
  }
922
+ }
933
923
 
934
- onData(data) {
935
- if (!this.#next) {
936
- this.#str += this.#decoder.decode(data, { stream: true })
937
- } else {
938
- const lines = this.#decoder.decode(data, { stream: true }).split(/\r?\n+/)
939
- lines[0] = this.#str + lines[0]
940
- this.#str = lines.pop() ?? ''
941
-
942
- const rows = []
943
- for (const line of lines) {
944
- if (this.#state === 0) {
945
- if (line.endsWith('[')) {
946
- this.#state = 1
947
- } else {
948
- throw new Error('unexpected data')
949
- }
950
- } else if (this.#state === 1) {
951
- if (line.startsWith(']')) {
952
- this.#state = 2
953
- } else {
954
- rows.push(JSON.parse(line.slice(0, line.lastIndexOf('}') + 1)))
955
- }
956
- }
957
- }
924
+ class ErrorHandler extends undici.DecoratorHandler {
925
+ #str
926
+ #decoder
927
+ #statusCode
928
+ #statusText
929
+ #contentType
930
+ #opts
931
+ #handler
932
+ #error
958
933
 
959
- if (rows.length === 0) {
960
- return true
961
- }
934
+ constructor({ opts, handler }) {
935
+ super(handler)
962
936
 
963
- const thenable = this.#next(rows)
937
+ this.#handler = handler
938
+ this.#opts = opts
939
+ }
964
940
 
965
- if (!thenable) {
966
- return true
967
- }
941
+ onConnect(...args) {
942
+ this.#str = ''
943
+ this.#decoder = null
944
+ this.#handler.onConnect?.(...args)
945
+ }
968
946
 
969
- thenable.then(this.#resume, this.#abort)
947
+ onHeaders(statusCode, headers, resume, statusText) {
948
+ this.#statusCode = statusCode
949
+ this.#statusText = statusText
950
+
951
+ if (this.#statusCode < 200 || this.#statusCode > 300) {
952
+ headers = Array.isArray(headers) ? undiciUtil.parseHeaders(headers) : headers
953
+
954
+ this.#error = new Error(this.#statusText ?? this.#statusCode)
970
955
 
971
- return false
956
+ this.#contentType = parseContentType(headers['content-type'] ?? '')?.type
957
+ if (this.#contentType !== 'application/json' && this.#contentType !== 'plain/text') {
958
+ throw this.#error
959
+ }
960
+
961
+ this.#decoder = new TextDecoder()
962
+ } else {
963
+ return this.#handler.onHeaders?.(statusCode, headers, resume, statusText)
972
964
  }
973
965
  }
974
966
 
975
- onComplete() {
976
- assert(!this.#next || this.#state === 2)
967
+ onData(data) {
968
+ if (this.#error) {
969
+ this.#str += this.#decoder?.decode(data, { stream: true }) ?? ''
970
+ } else {
971
+ return this.#handler?.onData(data)
972
+ }
973
+ }
977
974
 
978
- if (!this.#next) {
979
- this.#str += this.#decoder.decode(undefined, { stream: false })
975
+ onComplete(...args) {
976
+ if (this.#error) {
977
+ this.#str += this.#decoder?.decode(undefined, { stream: false }) ?? ''
980
978
 
981
- if (this.#statusCode >= 200 && this.#statusCode < 300) {
982
- this.onFinally(null, this.#next ? null : JSON.parse(this.#str))
983
- } else {
979
+ if (this.#contentType === 'application/json') {
984
980
  let body
985
981
  let reason
986
982
  let error
@@ -989,46 +985,167 @@ class Handler {
989
985
  reason = body.reason
990
986
  error = body.error
991
987
  } catch {
988
+ body = this.#str
992
989
  // Do nothing...
993
990
  }
994
991
 
995
- this.onFinally(
996
- Object.assign(new Error(this.#statusText ?? reason), {
997
- reason,
998
- error,
999
- data: {
1000
- req: this.#opts,
1001
- res: {
1002
- body,
1003
- statusCode: this.#statusCode,
1004
- },
992
+ throw Object.assign(this.#error, {
993
+ reason,
994
+ error,
995
+ data: {
996
+ req: this.#opts,
997
+ res: {
998
+ body,
999
+ statusCode: this.#statusCode,
1000
+ },
1001
+ },
1002
+ })
1003
+ } else if (this.#contentType === 'plain/text') {
1004
+ throw Object.assign(this.#error, {
1005
+ data: {
1006
+ req: this.#opts,
1007
+ res: {
1008
+ body: this.#str,
1009
+ statusCode: this.#statusCode,
1005
1010
  },
1006
- }),
1007
- )
1011
+ },
1012
+ })
1008
1013
  }
1014
+
1015
+ assert(false)
1009
1016
  } else {
1010
- this.onFinally(null)
1017
+ this.#handler.onComplete?.(...args)
1011
1018
  }
1012
1019
  }
1020
+ }
1013
1021
 
1014
- onError(err) {
1015
- this.onFinally(err)
1022
+ class StreamOutput extends stream.Readable {
1023
+ #str
1024
+ #resume
1025
+ #abort
1026
+ #state
1027
+ #decoder
1028
+ #batched
1029
+ #didPush = false
1030
+
1031
+ constructor({ batched = false, highWaterMark = batched ? 2 : 128 }) {
1032
+ super({ objectMode: true, highWaterMark })
1033
+ this.#batched = batched
1016
1034
  }
1017
1035
 
1018
- onFinally(err, val) {
1019
- if (this.#signal) {
1020
- this.#signal.removeEventListener('abort', this.#onAbort)
1021
- }
1036
+ _read() {
1037
+ this.#resume?.()
1038
+ }
1022
1039
 
1023
- if (err) {
1024
- this.#reject(err)
1040
+ _destroy(err, callback) {
1041
+ this.#abort?.(err)
1042
+ callback(err)
1043
+ }
1044
+
1045
+ onConnect(abort) {
1046
+ if (this.destroyed) {
1047
+ abort(this.errored)
1048
+ } else if (!this.#didPush) {
1049
+ this.#state = 0
1050
+ this.#str = ''
1051
+ this.#abort = abort
1052
+ this.#decoder = new TextDecoder()
1025
1053
  } else {
1026
- this.#resolve(val)
1054
+ throw new Error('unusable')
1027
1055
  }
1028
1056
  }
1057
+
1058
+ onHeaders(statusCode, rawHeaders, resume, statusText) {
1059
+ if (statusCode >= 300 || statusCode < 200) {
1060
+ throw new Error('invalid status code: ' + statusCode)
1061
+ }
1062
+
1063
+ this.#resume = resume
1064
+ }
1065
+
1066
+ onData(data) {
1067
+ const lines = this.#decoder.decode(data, { stream: true }).split(/\r?\n+/)
1068
+ lines[0] = this.#str + lines[0]
1069
+ this.#str = lines.pop() ?? ''
1070
+
1071
+ const rows = this.#batched ? [] : null
1072
+ for (const line of lines) {
1073
+ if (this.#state === 0) {
1074
+ if (line.endsWith('[')) {
1075
+ this.#state = 1
1076
+ } else {
1077
+ throw new Error('unexpected data')
1078
+ }
1079
+ } else if (this.#state === 1) {
1080
+ if (line.startsWith(']')) {
1081
+ this.#state = 2
1082
+ } else {
1083
+ const row = JSON.parse(line.slice(0, line.lastIndexOf('}') + 1))
1084
+ if (rows) {
1085
+ rows.push(row)
1086
+ } else {
1087
+ this.push(row)
1088
+ this.#didPush = true
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ if (rows) {
1095
+ this.push(rows)
1096
+ this.#didPush = true
1097
+ }
1098
+
1099
+ return this.readableLength < this.readableHighWaterMark
1100
+ }
1101
+
1102
+ onComplete() {
1103
+ assert(this.#state === 2)
1104
+ this.push(null)
1105
+ }
1106
+
1107
+ onError(err) {
1108
+ this.destroy(err)
1109
+ }
1029
1110
  }
1030
1111
 
1031
- export async function request(
1112
+ class PromiseOutput {
1113
+ #resolve
1114
+ #reject
1115
+ #str
1116
+ #decoder
1117
+
1118
+ constructor({ resolve, reject }) {
1119
+ this.#resolve = resolve
1120
+ this.#reject = reject
1121
+ }
1122
+
1123
+ onConnect() {
1124
+ this.#decoder = new TextDecoder()
1125
+ this.#str = ''
1126
+ }
1127
+
1128
+ onHeaders(statusCode, rawHeaders, resume, statusText) {
1129
+ if (statusCode >= 300 || statusCode < 200) {
1130
+ throw new Error('invalid status code: ' + statusCode)
1131
+ }
1132
+ }
1133
+
1134
+ onData(data) {
1135
+ this.#str += this.#decoder.decode(data, { stream: true })
1136
+ }
1137
+
1138
+ onComplete() {
1139
+ this.#str += this.#decoder.decode(undefined, { stream: false })
1140
+ this.#resolve(JSON.parse(this.#str))
1141
+ }
1142
+
1143
+ onError(err) {
1144
+ this.#reject(err)
1145
+ }
1146
+ }
1147
+
1148
+ export function request(
1032
1149
  url,
1033
1150
  {
1034
1151
  body,
@@ -1037,8 +1154,8 @@ export async function request(
1037
1154
  headers,
1038
1155
  dispatcher = undici.getGlobalDispatcher(),
1039
1156
  signal,
1157
+ stream,
1040
1158
  },
1041
- next,
1042
1159
  ) {
1043
1160
  url = new URL(url)
1044
1161
  if (url.search) {
@@ -1050,12 +1167,27 @@ export async function request(
1050
1167
  method,
1051
1168
  headers: {
1052
1169
  'content-type': body != null && typeof body === 'object' ? 'application/json' : 'plain/text',
1170
+ 'user-agent': globalThis.userAgent,
1171
+ 'request-id': genReqId(),
1053
1172
  accept: 'application/json',
1054
1173
  ...headers,
1055
1174
  },
1056
1175
  body: body != null && typeof body === 'object' ? JSON.stringify(body) : body,
1057
1176
  }
1058
- return new Promise((resolve, reject) => {
1059
- dispatcher.dispatch(opts, new Handler({ opts, signal, resolve, reject, next }))
1060
- })
1177
+
1178
+ dispatcher = dispatcher.compose(
1179
+ (dispatch) => (opts, handler) => dispatch(opts, new ErrorHandler({ opts, handler })),
1180
+ (dispatch) => (opts, handler) => dispatch(opts, new SignalHandler({ signal, handler })),
1181
+ )
1182
+
1183
+ if (stream) {
1184
+ const handler = new StreamOutput(stream)
1185
+ dispatcher.dispatch(opts, handler)
1186
+ return handler
1187
+ } else {
1188
+ return new Promise((resolve, reject) => {
1189
+ const handler = new PromiseOutput({ resolve, reject })
1190
+ dispatcher.dispatch(opts, handler)
1191
+ })
1192
+ }
1061
1193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "19.1.0",
3
+ "version": "19.1.2",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -75,10 +75,11 @@
75
75
  "/__tests__"
76
76
  ],
77
77
  "dependencies": {
78
- "@aws-sdk/client-s3": "^3.552.0",
78
+ "@aws-sdk/client-s3": "^3.554.0",
79
79
  "@elastic/elasticsearch": "^8.13.1",
80
- "@elastic/transport": "^8.5.0",
81
- "@nxtedition/nxt-undici": "^2.0.45",
80
+ "@elastic/transport": "^8.5.1",
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",
@@ -94,13 +95,13 @@
94
95
  "p-queue": "^8.0.1",
95
96
  "pino": "^8.20.0",
96
97
  "pino-std-serializers": "^6.2.2",
97
- "qs": "^6.12.0",
98
+ "qs": "^6.12.1",
98
99
  "request-target": "^1.0.2",
99
100
  "smpte-timecode": "^1.3.5",
100
101
  "split-string": "^6.0.0",
101
102
  "split2": "^4.2.0",
102
103
  "toobusy-js": "^0.5.1",
103
- "undici": "^6.12.0",
104
+ "undici": "^6.13.0",
104
105
  "url-join": "^5.0.0"
105
106
  },
106
107
  "devDependencies": {
@@ -111,7 +112,7 @@
111
112
  "eslint-config-prettier": "^9.1.0",
112
113
  "eslint-config-standard": "^17.0.0",
113
114
  "eslint-plugin-import": "^2.29.1",
114
- "eslint-plugin-n": "^17.1.0",
115
+ "eslint-plugin-n": "^17.2.1",
115
116
  "eslint-plugin-node": "^11.1.0",
116
117
  "eslint-plugin-promise": "^6.0.0",
117
118
  "husky": "^9.0.11",