@nxtedition/lib 19.0.4 → 19.0.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 (3) hide show
  1. package/couch.js +122 -90
  2. package/package.json +1 -1
  3. package/serializers.js +2 -0
package/couch.js CHANGED
@@ -7,6 +7,8 @@ import querystring from 'querystring'
7
7
  import urljoin from 'url-join'
8
8
  import undici from 'undici'
9
9
  import { AbortError } from './errors.js'
10
+ import split2 from 'split2'
11
+ import stream from 'node:stream'
10
12
 
11
13
  // https://github.com/fastify/fastify/blob/main/lib/reqIdGenFactory.js
12
14
  // 2,147,483,647 (2^31 − 1) stands for max SMI value (an internal optimization of V8).
@@ -226,21 +228,9 @@ export function makeCouch(opts) {
226
228
  }
227
229
  }
228
230
 
229
- async function* parse(live) {
230
- let remaining = Number(options.limit) || Infinity
231
-
232
- const params2 = {
233
- ...params,
234
- ...options.query,
235
- feed: live ? 'continuous' : 'normal',
236
- }
237
-
238
- if (Number.isFinite(remaining)) {
239
- params.limit = remaining
240
- }
241
-
231
+ async function parse(live, params) {
242
232
  const req = {
243
- path: `${dbPathname}/_changes?${new URLSearchParams(params2)}`,
233
+ path: `${dbPathname}/_changes?${new URLSearchParams(params)}`,
244
234
  idempotent: false,
245
235
  blocking: true,
246
236
  method,
@@ -258,96 +248,138 @@ export function makeCouch(opts) {
258
248
  const HEAD = '{"results":['
259
249
  const TAIL = '],'
260
250
 
261
- try {
262
- const res = await client.request(req)
263
-
264
- if (res.statusCode < 200 || res.statusCode >= 300) {
265
- throw makeError(req, {
266
- status: res.statusCode,
267
- headers: res.headers,
268
- data: await res.body.text(),
269
- })
270
- }
251
+ const res = await client.request(req)
271
252
 
272
- retryCount = 0
273
-
274
- const decoder = new TextDecoder()
275
-
276
- let str = ''
277
- let state = 0
278
- for await (const chunk of res.body) {
279
- str += decoder.decode(chunk, { stream: true })
280
-
281
- const lines = str.split('\n')
282
- str = lines.pop() ?? ''
253
+ if (res.statusCode < 200 || res.statusCode >= 300) {
254
+ throw makeError(req, {
255
+ status: res.statusCode,
256
+ headers: res.headers,
257
+ data: await res.body.text(),
258
+ })
259
+ }
283
260
 
284
- const changes = []
285
- for (const line of lines) {
286
- if (line === '') {
287
- continue
288
- }
289
- if (live) {
290
- const data = JSON.parse(line)
291
- if (data.last_seq) {
292
- assert(data.last_seq, 'invalid last_seq: ' + data.last_seq)
293
- params.since = data.last_seq
294
- } else {
295
- params.since = data.seq || params.since
296
- changes.push(data)
297
- }
298
- } else {
299
- // NOTE: This makes some assumptions about the format of the JSON.
300
- if (state === 0) {
301
- if (line === HEAD) {
302
- state = 1
261
+ return stream.pipeline(
262
+ res.body,
263
+ split2(),
264
+ new stream.Transform({
265
+ objectMode: true,
266
+ construct(callback) {
267
+ this.state = 0
268
+ callback(null)
269
+ },
270
+ transform(line, encoding, callback) {
271
+ assert(typeof line === 'string', 'invalid line: ' + line)
272
+ if (line) {
273
+ if (live) {
274
+ const data = JSON.parse(line)
275
+ if (data.last_seq) {
276
+ assert(data.last_seq, 'invalid last_seq: ' + data.last_seq)
277
+ params.since = data.last_seq
303
278
  } else {
304
- assert(line.length < HEAD.length, 'invalid line: ' + line)
305
- }
306
- } else if (state === 1) {
307
- if (line === TAIL) {
308
- state = 2
309
- } else {
310
- const idx = line.lastIndexOf('}') + 1
311
- assert(idx > 0, 'invalid line; ' + line)
312
- const data = JSON.parse(line.slice(0, idx))
313
279
  params.since = data.seq || params.since
314
- changes.push(data)
280
+ this.push(data)
315
281
  }
316
- } else if (state === 2) {
317
- state = 3
318
- params.since = JSON.parse('{' + line).last_seq
319
- assert(params.since, 'invalid last_seq: ' + params.since)
320
282
  } else {
321
- assert(false, 'invalid state: ' + state)
283
+ // NOTE: This makes some assumptions about the format of the JSON.
284
+ if (this.state === 0) {
285
+ if (line === HEAD) {
286
+ this.state = 1
287
+ } else {
288
+ assert(line.length < HEAD.length, 'invalid line: ' + line)
289
+ }
290
+ } else if (this.state === 1) {
291
+ if (line === TAIL) {
292
+ this.state = 2
293
+ } else {
294
+ const idx = line.lastIndexOf('}') + 1
295
+ assert(idx > 0, 'invalid line; ' + line)
296
+ const data = JSON.parse(line.slice(0, idx))
297
+ params.since = data.seq || params.since
298
+ this.push(data)
299
+ }
300
+ } else if (this.state === 2) {
301
+ this.state = 3
302
+ params.since = JSON.parse('{' + line).last_seq
303
+ assert(params.since, 'invalid last_seq: ' + params.since)
304
+ } else {
305
+ assert(false, 'invalid state: ' + this.state)
306
+ }
322
307
  }
323
308
  }
309
+ callback(null)
310
+ },
311
+ flush(callback) {
312
+ assert(live || this.state === 3, 'invalid state: ' + this.state)
313
+ callback(null)
314
+ },
315
+ }),
316
+ () => {},
317
+ )
318
+ }
319
+
320
+ let remaining = Number(options.limit) || Infinity
321
+ try {
322
+ while (true) {
323
+ try {
324
+ const params2 = {
325
+ ...params,
326
+ ...options.query,
327
+ feed: live ? 'continuous' : 'normal',
324
328
  }
325
329
 
326
- if (changes.length > 0) {
327
- if (batched) {
328
- yield changes
329
- } else {
330
- yield* changes
331
- }
330
+ if (Number.isFinite(remaining)) {
331
+ params.limit = remaining
332
332
  }
333
333
 
334
- remaining -= changes.length
335
- assert(remaining >= 0, 'invalid remaining: ' + remaining)
336
- }
334
+ const src = await parse(live, params2)
335
+ const changes = []
337
336
 
338
- assert(live || state === 3, 'invalid state: ' + state)
339
- assert(!str, 'invalid str: ' + str)
340
- } catch (err) {
341
- Object.assign(err, { data: req })
342
- throw err
343
- }
344
- }
337
+ let resume = null
338
+ let error = null
339
+ let ended = false
345
340
 
346
- try {
347
- while (true) {
348
- try {
349
- yield* parse(live)
350
- return
341
+ src
342
+ .on('readable', () => {
343
+ if (resume) {
344
+ resume()
345
+ resume = null
346
+ }
347
+ })
348
+ .on('error', (err) => {
349
+ error = err
350
+
351
+ if (resume) {
352
+ resume()
353
+ resume = null
354
+ }
355
+ })
356
+ .on('end', () => {
357
+ ended = true
358
+ })
359
+
360
+ while (true) {
361
+ const change = src.read()
362
+ if (change) {
363
+ changes.push(change)
364
+ } else if (changes.length) {
365
+ remaining -= changes.length
366
+ assert(remaining >= 0, 'invalid remaining: ' + remaining)
367
+
368
+ if (batched) {
369
+ yield changes.splice(0)
370
+ } else {
371
+ yield* changes.splice(0)
372
+ }
373
+ } else if (error) {
374
+ throw error
375
+ } else if (!ended) {
376
+ await new Promise((resolve) => {
377
+ resume = resolve
378
+ })
379
+ } else {
380
+ return
381
+ }
382
+ }
351
383
  } catch (err) {
352
384
  if (err.name === 'AbortError') {
353
385
  throw err
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "19.0.4",
3
+ "version": "19.0.6",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
package/serializers.js CHANGED
@@ -29,6 +29,8 @@ function getHeaders(obj) {
29
29
  }
30
30
 
31
31
  export default {
32
+ data: (data) =>
33
+ data != null && typeof data === 'object' ? JSON.stringify(data, undefined, 2) : data,
32
34
  err: (err) => {
33
35
  // TODO (fix): Merge with errors/serializeError?
34
36