@nxtedition/lib 15.0.44 → 15.0.46

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/couch.js +57 -38
  2. package/deepstream.js +111 -6
  3. package/http.js +8 -15
  4. package/package.json +2 -1
package/couch.js CHANGED
@@ -1,7 +1,9 @@
1
+ const stream = require('node:stream')
2
+ const tp = require('node:timers/promises')
1
3
  const createError = require('http-errors')
2
4
  const makeWeak = require('./weakCache')
3
- const tp = require('timers/promises')
4
5
  const { delay } = require('./http')
6
+ const split2 = require('split2')
5
7
 
6
8
  // https://github.com/fastify/fastify/blob/main/lib/reqIdGenFactory.js
7
9
  // 2,147,483,647 (2^31 − 1) stands for max SMI value (an internal optimization of V8).
@@ -62,7 +64,7 @@ module.exports = function (opts) {
62
64
  }
63
65
 
64
66
  const { origin: dbOrigin, pathname: dbPathname } = new URL(
65
- Array.isArray(config.url) ? config.url[0] : config.url
67
+ Array.isArray(config.url) ? config.url[0] : config.url,
66
68
  )
67
69
 
68
70
  const defaultClientOpts = {
@@ -83,7 +85,7 @@ module.exports = function (opts) {
83
85
  ...defaultClientOpts,
84
86
  connections: 4, // TODO (fix): Global limit?
85
87
  pipelining: 2,
86
- })
88
+ }),
87
89
  )
88
90
 
89
91
  function makeError(req, res) {
@@ -131,7 +133,7 @@ module.exports = function (opts) {
131
133
  })
132
134
  }
133
135
 
134
- async function* changes({ client = defaultClient, signal, ...options } = {}) {
136
+ async function* changes({ client = defaultClient, signal, highWaterMark, ...options } = {}) {
135
137
  const params = {}
136
138
 
137
139
  let body
@@ -242,42 +244,59 @@ module.exports = function (opts) {
242
244
  ...(body ? { 'content-type': 'application/json' } : {}),
243
245
  },
244
246
  throwOnError: true,
245
- highWaterMark: 128 * 1024, // TODO (fix): Needs support in undici...
247
+ highWaterMark: highWaterMark ?? 256 * 1024,
246
248
  bodyTimeout: 2 * (params.heartbeat || 60e3),
247
249
  }
248
250
 
249
- try {
250
- const res = await client.request(req)
251
+ const res = await client.request(req)
251
252
 
252
- retryCount = 0
253
+ retryCount = 0
253
254
 
254
- let str = ''
255
- for await (const chunk of res.body) {
256
- const lines = (str + chunk).split('\n')
257
- str = lines.pop() ?? ''
258
-
259
- const results = batched ? [] : null
260
- for (const line of lines) {
261
- if (line) {
262
- const change = JSON.parse(line)
263
- if (change.seq) {
264
- params.since = change.seq
265
- }
266
- if (results) {
267
- results.push(change)
268
- } else {
269
- yield change
270
- }
271
- }
272
- }
255
+ const src = stream.pipeline(res.body, split2(), () => {})
256
+
257
+ let error
258
+ let ended = false
259
+ let resume = () => {}
260
+
261
+ src
262
+ .on('error', (err) => {
263
+ error = err
264
+ })
265
+ .on('readable', () => {
266
+ resume()
267
+ })
268
+ .on('end', () => {
269
+ ended = true
270
+ resume()
271
+ })
273
272
 
274
- if (results?.length) {
275
- yield results
273
+ const batch = batched ? [] : null
274
+ while (true) {
275
+ const line = src.read()
276
+
277
+ if (line === '') {
278
+ continue
279
+ } else if (line !== null) {
280
+ const change = JSON.parse(line)
281
+ if (change.seq) {
282
+ params.since = change.seq
283
+ }
284
+ if (batch) {
285
+ batch.push(change)
286
+ } else {
287
+ yield change
276
288
  }
289
+ } else if (batch?.length) {
290
+ yield batch.splice(0)
291
+ } else if (error) {
292
+ throw Object.assign(error, { data: req })
293
+ } else if (ended) {
294
+ return
295
+ } else {
296
+ await new Promise((resolve) => {
297
+ resume = resolve
298
+ })
277
299
  }
278
- } catch (err) {
279
- Object.assign(err, { data: req })
280
- throw err
281
300
  }
282
301
  }
283
302
 
@@ -416,7 +435,7 @@ module.exports = function (opts) {
416
435
 
417
436
  function _request(
418
437
  url,
419
- { params, client = defaultClient, idempotent, body, method, headers, signal }
438
+ { params, client = defaultClient, idempotent, body, method, headers, signal },
420
439
  ) {
421
440
  if (Array.isArray(headers)) {
422
441
  // Do nothing...
@@ -445,7 +464,7 @@ module.exports = function (opts) {
445
464
  url
446
465
  .split('/')
447
466
  .map((part) => encodeURIComponent(part))
448
- .join('/')
467
+ .join('/'),
449
468
  )
450
469
  }
451
470
 
@@ -515,7 +534,7 @@ module.exports = function (opts) {
515
534
  status: this.status,
516
535
  headers: this.headers,
517
536
  data: this.data,
518
- })
537
+ }),
519
538
  )
520
539
  } else {
521
540
  this.resolve({
@@ -536,7 +555,7 @@ module.exports = function (opts) {
536
555
 
537
556
  this.reject(err)
538
557
  },
539
- })
558
+ }),
540
559
  )
541
560
  }
542
561
 
@@ -721,7 +740,7 @@ module.exports = function (opts) {
721
740
  pathname,
722
741
  params,
723
742
  body,
724
- { client = getClient('_all_docs'), signal, idempotent = true, headers } = {}
743
+ { client = getClient('_all_docs'), signal, idempotent = true, headers } = {},
725
744
  ) {
726
745
  const req = {
727
746
  pathname,
@@ -746,7 +765,7 @@ module.exports = function (opts) {
746
765
  pathname,
747
766
  params,
748
767
  body,
749
- { client, signal, idempotent = true, headers } = {}
768
+ { client, signal, idempotent = true, headers } = {},
750
769
  ) {
751
770
  const req = {
752
771
  pathname,
package/deepstream.js CHANGED
@@ -1,5 +1,9 @@
1
1
  const qs = require('qs')
2
2
  const cached = require('./util/cached')
3
+ const undici = require('undici')
4
+ const stream = require('node:stream')
5
+ const split2 = require('split2')
6
+ const { delay } = require('./http')
3
7
 
4
8
  function provide(ds, domain, callback, options) {
5
9
  if (domain instanceof RegExp) {
@@ -23,7 +27,7 @@ function provide(ds, domain, callback, options) {
23
27
  callback = cached(
24
28
  callback,
25
29
  cachedOptions,
26
- cachedOptions.keySelector ? cachedOptions.keySelector : (id, options, key) => key
30
+ cachedOptions.keySelector ? cachedOptions.keySelector : (id, options, key) => key,
27
31
  )
28
32
  } else if (options.minAge) {
29
33
  // Backwards compat
@@ -43,13 +47,13 @@ function provide(ds, domain, callback, options) {
43
47
  const [id, options] = parseKey(key)
44
48
  return callback(id, options, key)
45
49
  },
46
- { recursive: options.recursive, mode: options.mode, stringify: options.stringify }
50
+ { recursive: options.recursive, mode: options.mode, stringify: options.stringify },
47
51
  )
48
52
  }
49
53
 
50
54
  function parseKey(key) {
51
55
  const { json, id, query } = key.match(
52
- /^(?:(?<json>\{.*\}):|(?<id>.*):)?[^?]*(?:\?(?<query>.*))?$/
56
+ /^(?:(?<json>\{.*\}):|(?<id>.*):)?[^?]*(?:\?(?<query>.*))?$/,
53
57
  ).groups
54
58
  return [
55
59
  id || '',
@@ -75,7 +79,7 @@ function observe(ds, name, ...args) {
75
79
  ? `${name.endsWith('?') ? '' : '?'}${qs.stringify(query, { skipNulls: true })}`
76
80
  : ''
77
81
  }`,
78
- ...args
82
+ ...args,
79
83
  )
80
84
  }
81
85
 
@@ -94,7 +98,7 @@ function observe2(ds, name, ...args) {
94
98
  ? `${name.endsWith('?') ? '' : '?'}${qs.stringify(query, { skipNulls: true })}`
95
99
  : ''
96
100
  }`,
97
- ...args
101
+ ...args,
98
102
  )
99
103
  }
100
104
 
@@ -113,7 +117,7 @@ function get(ds, name, ...args) {
113
117
  ? `${name.endsWith('?') ? '' : '?'}${qs.stringify(query, { skipNulls: true })}`
114
118
  : ''
115
119
  }`,
116
- ...args
120
+ ...args,
117
121
  )
118
122
  }
119
123
 
@@ -127,18 +131,119 @@ function init(ds) {
127
131
  set: (...args) => ds.record.set(...args),
128
132
  get: (...args) => get(ds, ...args),
129
133
  update: (...args) => ds.record.update(...args),
134
+ changes: (...args) => changes(ds, ...args),
130
135
  },
131
136
  }
132
137
  ds.nxt = nxt
133
138
  return nxt
134
139
  }
135
140
 
141
+ async function* changes(
142
+ ds,
143
+ {
144
+ since = 'now',
145
+ live = true,
146
+ batched = false,
147
+ includeDocs = false,
148
+ highWaterMark = 256 * 1024,
149
+ heartbeat = 60e3,
150
+ retry,
151
+ },
152
+ ) {
153
+ const url = new URL('/_record/changes', ds._url)
154
+
155
+ url.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
156
+ url.port = '6100'
157
+ url.searchParams.set('since', since || '0')
158
+ url.searchParams.set('live', String(live))
159
+ url.searchParams.set('include_docs', String(includeDocs))
160
+
161
+ for (let retryCount = 0; retryCount < retry; retryCount++) {
162
+ const ac = new AbortController()
163
+ const signal = ac.signal
164
+ try {
165
+ // TODO (fix): Use nxt-undici
166
+ const res = await undici.request(url, {
167
+ idempotent: false,
168
+ blocking: true,
169
+ method: 'GET',
170
+ signal,
171
+ throwOnError: true,
172
+ highWaterMark,
173
+ bodyTimeout: 2 * heartbeat,
174
+ })
175
+
176
+ const src = stream.pipeline(res.body, split2(), () => {})
177
+
178
+ let error
179
+ let ended = false
180
+ let resume = () => {}
181
+
182
+ src
183
+ .on('error', (err) => {
184
+ error = err
185
+ })
186
+ .on('readable', () => {
187
+ resume()
188
+ })
189
+ .on('end', () => {
190
+ ended = true
191
+ resume()
192
+ })
193
+
194
+ const batch = batched ? [] : null
195
+ while (true) {
196
+ const line = src.read()
197
+
198
+ if (line === '') {
199
+ continue
200
+ } else if (line !== null) {
201
+ const change = JSON.parse(line)
202
+
203
+ retryCount = 0
204
+
205
+ if (change.seq) {
206
+ since = change.seq
207
+ }
208
+ if (batch) {
209
+ batch.push(change)
210
+ } else {
211
+ yield change
212
+ }
213
+ } else if (batch?.length) {
214
+ yield batch.splice(0)
215
+ } else if (error) {
216
+ throw error
217
+ } else if (ended) {
218
+ return
219
+ } else {
220
+ await new Promise((resolve) => {
221
+ resume = resolve
222
+ })
223
+ }
224
+ }
225
+ } catch (err) {
226
+ if (typeof retry === 'function') {
227
+ const retryState = { since }
228
+ await retry(err, retryCount, retryState, { signal })
229
+ url.searchParams.set('since', since || '0')
230
+ } else {
231
+ await delay(err, retryCount, { signal })
232
+ }
233
+ } finally {
234
+ ac.abort()
235
+ }
236
+ }
237
+ }
238
+
136
239
  module.exports = Object.assign(init, {
240
+ changes,
137
241
  provide,
138
242
  observe,
139
243
  observe2,
140
244
  get,
141
245
  record: {
246
+ changes,
142
247
  provide,
143
248
  observe,
144
249
  observe2,
package/http.js CHANGED
@@ -66,43 +66,36 @@ module.exports.request = async function request(ctx, next) {
66
66
  .on('timeout', function () {
67
67
  this.destroy(new createError.RequestTimeout())
68
68
  })
69
- .on('error', (err) => {
69
+ .on('error', function (err) {
70
70
  reqLogger.error({ err }, 'response error')
71
71
  reject(err)
72
72
  })
73
73
  .on('close', function () {
74
74
  reqLogger.debug('response closed')
75
- if (!this.writableEnded) {
76
- reject(
77
- Object.assign(new Error('response closed prematurely'), {
78
- code: 'ERR_STREAM_PREMATURE_CLOSE',
79
- }),
80
- )
81
- } else {
82
- resolve(null)
83
- }
75
+ resolve(null)
84
76
  })
85
77
  req
86
78
  .on('timeout', function () {
87
79
  this.destroy(new createError.RequestTimeout())
88
80
  })
89
- .on('error', (err) => {
81
+ .on('error', function (err) {
90
82
  reqLogger.error({ err }, 'request error')
91
83
  })
92
- .on('close', () => {
84
+ .on('close', function () {
93
85
  reqLogger.debug('request closed')
94
86
  })
95
87
  }),
96
88
  ])
97
89
 
98
- assert(res.writableEnded)
99
- assert(res.statusCode)
90
+ assert(req.aborted || res.writableEnded)
100
91
 
101
92
  const responseTime = Math.round(performance.now() - startTime)
102
93
 
103
94
  reqLogger = reqLogger.child({ res, responseTime })
104
95
 
105
- if (res.statusCode >= 500) {
96
+ if (req.aborted) {
97
+ reqLogger.debug('request aborted')
98
+ } else if (res.statusCode >= 500) {
106
99
  reqLogger.error('request error')
107
100
  } else if (res.statusCode >= 400) {
108
101
  reqLogger.warn('request failed')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "15.0.44",
3
+ "version": "15.0.46",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "files": [
@@ -89,6 +89,7 @@
89
89
  "request-target": "^1.0.2",
90
90
  "smpte-timecode": "^1.3.3",
91
91
  "split-string": "^6.0.0",
92
+ "split2": "^4.2.0",
92
93
  "toobusy-js": "^0.5.1",
93
94
  "undici": "^5.25.4",
94
95
  "url-join": "^4.0.0"