@nxtedition/lib 15.0.45 → 15.0.47

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 +57 -38
  2. package/deepstream.js +124 -6
  3. 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,132 @@ 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
+ signal,
151
+ retry,
152
+ },
153
+ ) {
154
+ const url = new URL('/_record/changes', ds._url)
155
+
156
+ url.protocol = url.protocol === 'ws:' ? 'http:' : 'https:'
157
+ url.port = '6100'
158
+ url.searchParams.set('since', since || '0')
159
+ url.searchParams.set('live', String(live))
160
+ url.searchParams.set('include_docs', String(includeDocs))
161
+
162
+ let ac
163
+
164
+ const abort = () => {
165
+ ac?.abort(signal.reason)
166
+ }
167
+
168
+ signal?.addEventListener('abort', abort)
169
+
170
+ try {
171
+ for (let retryCount = 0; retryCount < retry; retryCount++) {
172
+ ac = new AbortController()
173
+ try {
174
+ // TODO (fix): Use nxt-undici
175
+ const res = await undici.request(url, {
176
+ idempotent: false,
177
+ blocking: true,
178
+ method: 'GET',
179
+ signal: ac.signal,
180
+ throwOnError: true,
181
+ highWaterMark,
182
+ bodyTimeout: 2 * heartbeat,
183
+ })
184
+
185
+ const src = stream.pipeline(res.body, split2(), () => {})
186
+
187
+ let error
188
+ let ended = false
189
+ let resume = () => {}
190
+
191
+ src
192
+ .on('error', (err) => {
193
+ error = err
194
+ })
195
+ .on('readable', () => {
196
+ resume()
197
+ })
198
+ .on('end', () => {
199
+ ended = true
200
+ resume()
201
+ })
202
+
203
+ const batch = batched ? [] : null
204
+ while (true) {
205
+ const line = src.read()
206
+
207
+ if (line === '') {
208
+ continue
209
+ } else if (line !== null) {
210
+ const change = JSON.parse(line)
211
+
212
+ retryCount = 0
213
+
214
+ if (change.seq) {
215
+ since = change.seq
216
+ }
217
+ if (batch) {
218
+ batch.push(change)
219
+ } else {
220
+ yield change
221
+ }
222
+ } else if (batch?.length) {
223
+ yield batch.splice(0)
224
+ } else if (error) {
225
+ throw error
226
+ } else if (ended) {
227
+ return
228
+ } else {
229
+ await new Promise((resolve) => {
230
+ resume = resolve
231
+ })
232
+ }
233
+ }
234
+ } catch (err) {
235
+ if (typeof retry === 'function') {
236
+ const retryState = { since }
237
+ await retry(err, retryCount, retryState, { signal: ac.signal })
238
+ url.searchParams.set('since', since || '0')
239
+ } else {
240
+ await delay(err, retryCount, { signal: ac.signal })
241
+ }
242
+ } finally {
243
+ ac.abort()
244
+ ac = null
245
+ }
246
+ }
247
+ } finally {
248
+ signal?.removeEventListener('abort', abort)
249
+ }
250
+ }
251
+
136
252
  module.exports = Object.assign(init, {
253
+ changes,
137
254
  provide,
138
255
  observe,
139
256
  observe2,
140
257
  get,
141
258
  record: {
259
+ changes,
142
260
  provide,
143
261
  observe,
144
262
  observe2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "15.0.45",
3
+ "version": "15.0.47",
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"