hide-a-bed 3.0.1 → 4.0.1

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 (58) hide show
  1. package/README.md +104 -0
  2. package/cjs/impl/bulk.cjs +55 -6
  3. package/cjs/impl/crud.cjs +57 -7
  4. package/cjs/impl/errors.cjs +63 -0
  5. package/cjs/impl/logger.cjs +61 -0
  6. package/cjs/impl/patch.cjs +10 -5
  7. package/cjs/impl/query.cjs +11 -1
  8. package/cjs/impl/retry.cjs +54 -0
  9. package/cjs/impl/stream.cjs +48 -7
  10. package/cjs/index.cjs +26 -2
  11. package/cjs/schema/bind.cjs +40 -0
  12. package/cjs/schema/bulk.cjs +14 -0
  13. package/cjs/schema/config.cjs +20 -1
  14. package/cjs/schema/crud.cjs +9 -1
  15. package/cjs/schema/patch.cjs +6 -6
  16. package/cjs/schema/query.cjs +28 -19
  17. package/cjs/schema/stream.cjs +42 -0
  18. package/impl/bulk.mjs +58 -6
  19. package/impl/crud.mjs +67 -6
  20. package/impl/errors.mjs +53 -0
  21. package/impl/logger.mjs +59 -0
  22. package/impl/patch.mjs +36 -4
  23. package/impl/query.mjs +38 -2
  24. package/impl/retry.mjs +39 -0
  25. package/impl/stream.mjs +72 -17
  26. package/index.mjs +34 -4
  27. package/package.json +1 -1
  28. package/schema/bind.mjs +21 -0
  29. package/schema/bulk.mjs +17 -0
  30. package/schema/config.mjs +22 -1
  31. package/schema/crud.mjs +12 -0
  32. package/schema/patch.mjs +10 -6
  33. package/schema/query.mjs +30 -18
  34. package/schema/stream.mjs +23 -0
  35. package/build/build.mjs +0 -16
  36. package/build/build.rewrite-imports.mjs +0 -14
  37. package/impl/bulk.d.mts +0 -7
  38. package/impl/bulk.d.mts.map +0 -1
  39. package/impl/crud.d.mts +0 -10
  40. package/impl/crud.d.mts.map +0 -1
  41. package/impl/patch.d.mts +0 -2
  42. package/impl/patch.d.mts.map +0 -1
  43. package/impl/query.d.mts +0 -104
  44. package/impl/query.d.mts.map +0 -1
  45. package/impl/stream.d.mts +0 -2
  46. package/impl/stream.d.mts.map +0 -1
  47. package/index.d.mts +0 -34
  48. package/index.d.mts.map +0 -1
  49. package/schema/bulk.d.mts +0 -100
  50. package/schema/bulk.d.mts.map +0 -1
  51. package/schema/config.d.mts +0 -9
  52. package/schema/config.d.mts.map +0 -1
  53. package/schema/crud.d.mts +0 -83
  54. package/schema/crud.d.mts.map +0 -1
  55. package/schema/patch.d.mts +0 -47
  56. package/schema/patch.d.mts.map +0 -1
  57. package/schema/query.d.mts +0 -151
  58. package/schema/query.d.mts.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,34 @@
1
1
  API
2
2
  -------------
3
3
 
4
+ ### Setup
5
+
6
+ Depending on your environment, use import or require
7
+
8
+ ```
9
+ import { get, put, patch, remove, bulkSave, bulkGet, bulkRemove, query } from 'hide-a-bed'
10
+ ```
11
+ ```
12
+ const { get, put, patch, remove, bulkSave, bulkGet, bulkRemove, query } = require('hide-a-bed')
13
+ ```
14
+
15
+ ### Config
16
+
17
+ Anywhere you see a config, it is an object with the following setup
18
+
19
+ ```
20
+ { couch: 'https://username:pass@the.couch.url.com:5984' }
21
+ ```
22
+ Couch get is weird. We have chosen to return ```undefined``` if the doc is not found. All other things throw. If you want
23
+ not_found to also throw an exception, add the following to your config:
24
+
25
+ ```
26
+ { throwOnGetNotFound: true, couch: '...' }
27
+ ```
28
+
4
29
  ### Document Operations
5
30
 
31
+
6
32
  #### get(config, id)
7
33
  Get a single document by ID.
8
34
  - `config`: Object with `couch` URL string
@@ -165,4 +191,82 @@ const result = await query(config, view, options)
165
191
  // }
166
192
  ```
167
193
 
194
+ Logging Support
195
+ ==============
196
+
197
+ The library supports flexible logging options that can be configured through the config object:
198
+
199
+ ```javascript
200
+ // Enable console logging (error, warn, info, debug)
201
+ const config = {
202
+ couch: 'http://localhost:5984/mydb',
203
+ useConsoleLogger: true
204
+ }
205
+
206
+ // Use a custom logger object (winston-style)
207
+ const config = {
208
+ couch: 'http://localhost:5984/mydb',
209
+ logger: {
210
+ error: (msg) => console.error(msg),
211
+ warn: (msg) => console.warn(msg),
212
+ info: (msg) => console.info(msg),
213
+ debug: (msg) => console.debug(msg)
214
+ }
215
+ }
216
+
217
+ // Use a simple function logger
218
+ const config = {
219
+ couch: 'http://localhost:5984/mydb',
220
+ logger: (level, ...args) => console.log(level, ...args)
221
+ }
222
+ ```
223
+
224
+ The logger will track operations including:
225
+ - Document operations (get, put, patch)
226
+ - Bulk operations
227
+ - View queries
228
+ - Streaming operations
229
+ - Retries and error handling
230
+
231
+ Each operation logs appropriate information at these levels:
232
+ - error: Fatal/unrecoverable errors
233
+ - warn: Retryable errors, conflicts
234
+ - info: Operation start/completion
235
+ - debug: Detailed operation information
236
+
237
+
238
+ Streaming Support
239
+ =================
240
+
241
+ Want to stream data from couch? You can with queryStream. It looks identical to query, except you add an extra 'onRow' function
242
+
243
+ Here is a small hapi example of streaming data from couch to the client as ndjson.
244
+ We do a small transform by only streaming the doc. you can do a lot of things in the onrow function.
245
+
246
+ ```
247
+ import Hapi from '@hapi/hapi';
248
+ import { Readable } from 'stream';
249
+ import { queryStream } from bindConfig(process.env)
250
+ const view = '_design/users/_view/by_name'
251
+
252
+ const init = async () => {
253
+ const server = Hapi.server({ port: 3000 })
254
+ server.route({
255
+ method: 'GET',
256
+ path: '/stream',
257
+ handler: async (request, h) => {
258
+ const stream = new Readable({ read() {} });
259
+ const onRow = ({id, key, value, doc}) => stream.push(JSON.stringify(doc) + '\n')
260
+ const options = { startkey: req.query.startLetter, endkey: req.query.startLetter + '|', include_docs: true}
261
+ await queryStream(view, options, onRow)
262
+ stream.push(null) // end stream
263
+ return h.response(stream).type('application/x-ndjson');
264
+ }
265
+ })
266
+
267
+ await server.start();
268
+ console.log(`Server running on ${server.info.uri}`);
269
+ }
270
+ init()
271
+ ```
168
272
 
package/cjs/impl/bulk.cjs CHANGED
@@ -35,6 +35,8 @@ __export(bulk_exports, {
35
35
  module.exports = __toCommonJS(bulk_exports);
36
36
  var import_needle = __toESM(require("needle"), 1);
37
37
  var import_bulk = require("../schema/bulk.cjs");
38
+ var import_errors = require("./errors.cjs");
39
+ var import_logger = require("./logger.cjs");
38
40
  const opts = {
39
41
  json: true,
40
42
  headers: {
@@ -42,21 +44,65 @@ const opts = {
42
44
  }
43
45
  };
44
46
  const bulkSave = import_bulk.BulkSave.implement(async (config, docs) => {
45
- if (!docs) return { ok: false, error: "noDocs", reason: "no docs provided" };
46
- if (!docs.length) return { ok: false, error: "noDocs", reason: "no docs provided" };
47
+ const logger = (0, import_logger.createLogger)(config);
48
+ if (!docs) {
49
+ logger.warn("bulkSave called with no docs");
50
+ return { ok: false, error: "noDocs", reason: "no docs provided" };
51
+ }
52
+ if (!docs.length) {
53
+ logger.warn("bulkSave called with empty docs array");
54
+ return { ok: false, error: "noDocs", reason: "no docs provided" };
55
+ }
56
+ logger.info(`Starting bulk save of ${docs.length} documents`);
47
57
  const url = `${config.couch}/_bulk_docs`;
48
58
  const body = { docs };
49
- const resp = await (0, import_needle.default)("post", url, body, opts);
50
- if (resp.statusCode !== 201) throw new Error("could not save");
59
+ let resp;
60
+ try {
61
+ resp = await (0, import_needle.default)("post", url, body, opts);
62
+ } catch (err) {
63
+ logger.error("Network error during bulk save:", err);
64
+ import_errors.RetryableError.handleNetworkError(err);
65
+ }
66
+ if (!resp) {
67
+ logger.error("No response received from bulk save request");
68
+ throw new import_errors.RetryableError("no response", 503);
69
+ }
70
+ if (import_errors.RetryableError.isRetryableStatusCode(resp.statusCode)) {
71
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
72
+ throw new import_errors.RetryableError("retryable error during bulk save", resp.statusCode);
73
+ }
74
+ if (resp.statusCode !== 201) {
75
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
76
+ throw new Error("could not save");
77
+ }
51
78
  const results = resp?.body || [];
52
79
  return results;
53
80
  });
54
81
  const bulkGet = import_bulk.BulkGet.implement(async (config, ids) => {
82
+ const logger = (0, import_logger.createLogger)(config);
55
83
  const keys = ids;
84
+ logger.info(`Starting bulk get for ${keys.length} documents`);
56
85
  const url = `${config.couch}/_all_docs?include_docs=true`;
57
86
  const body = { keys };
58
- const resp = await (0, import_needle.default)("post", url, body, opts);
59
- if (resp.statusCode !== 200) throw new Error("could not fetch");
87
+ let resp;
88
+ try {
89
+ resp = await (0, import_needle.default)("post", url, body, opts);
90
+ } catch (err) {
91
+ logger.error("Network error during bulk get:", err);
92
+ import_errors.RetryableError.handleNetworkError(err);
93
+ }
94
+ if (!resp) {
95
+ logger.error("No response received from bulk get request");
96
+ throw new import_errors.RetryableError("no response", 503);
97
+ }
98
+ if (import_errors.RetryableError.isRetryableStatusCode(resp.statusCode)) {
99
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
100
+ throw new import_errors.RetryableError("retryable error during bulk get", resp.statusCode);
101
+ }
102
+ if (resp.statusCode !== 200) {
103
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
104
+ throw new Error("could not fetch");
105
+ }
60
106
  const rows = resp?.body?.rows || [];
61
107
  const docs = [];
62
108
  rows.forEach((r) => {
@@ -66,9 +112,12 @@ const bulkGet = import_bulk.BulkGet.implement(async (config, ids) => {
66
112
  const doc = r.doc;
67
113
  docs.push(doc);
68
114
  });
115
+ logger.info(`Successfully retrieved ${docs.length} documents`);
69
116
  return docs;
70
117
  });
71
118
  const bulkRemove = import_bulk.BulkRemove.implement(async (config, ids) => {
119
+ const logger = (0, import_logger.createLogger)(config);
120
+ logger.info(`Starting bulk remove for ${ids.length} documents`);
72
121
  const docs = await bulkGet(config, ids);
73
122
  docs.forEach((d) => {
74
123
  d._deleted = true;
package/cjs/impl/crud.cjs CHANGED
@@ -34,6 +34,8 @@ __export(crud_exports, {
34
34
  module.exports = __toCommonJS(crud_exports);
35
35
  var import_needle = __toESM(require("needle"), 1);
36
36
  var import_crud = require("../schema/crud.cjs");
37
+ var import_errors = require("./errors.cjs");
38
+ var import_logger = require("./logger.cjs");
37
39
  const opts = {
38
40
  json: true,
39
41
  headers: {
@@ -41,24 +43,72 @@ const opts = {
41
43
  }
42
44
  };
43
45
  const get = import_crud.CouchGet.implement(async (config, id) => {
46
+ const logger = (0, import_logger.createLogger)(config);
44
47
  const url = `${config.couch}/${id}`;
45
- const resp = await (0, import_needle.default)("get", url, opts);
46
- if (resp.statusCode === 404) return null;
47
- const result = resp?.body || {};
48
- if (resp.statusCode !== 200) {
49
- throw new Error(result.reason || "failed");
48
+ logger.info(`Getting document with id: ${id}`);
49
+ try {
50
+ const resp = await (0, import_needle.default)("get", url, opts);
51
+ if (!resp) {
52
+ logger.error("No response received from get request");
53
+ throw new import_errors.RetryableError("no response", 503);
54
+ }
55
+ if (resp.statusCode === 404) {
56
+ logger.debug(`Document not found: ${id}`);
57
+ return null;
58
+ }
59
+ const result = resp?.body || {};
60
+ if (resp.statusCode === 404) {
61
+ if (config.throwOnGetNotFound) {
62
+ logger.warn(`Document not found (throwing error): ${id}`);
63
+ throw new Error(result.reason || "not_found");
64
+ } else {
65
+ logger.debug(`Document not found (returning undefined): ${id}`);
66
+ return void 0;
67
+ }
68
+ }
69
+ if (import_errors.RetryableError.isRetryableStatusCode(resp.statusCode)) {
70
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
71
+ throw new import_errors.RetryableError(result.reason || "retryable error", resp.statusCode);
72
+ }
73
+ if (resp.statusCode !== 200) {
74
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
75
+ throw new Error(result.reason || "failed");
76
+ }
77
+ logger.info(`Successfully retrieved document: ${id}`);
78
+ return result;
79
+ } catch (err) {
80
+ logger.error("Error during get operation:", err);
81
+ import_errors.RetryableError.handleNetworkError(err);
50
82
  }
51
- return result;
52
83
  });
53
84
  const put = import_crud.CouchPut.implement(async (config, doc) => {
85
+ const logger = (0, import_logger.createLogger)(config);
54
86
  const url = `${config.couch}/${doc._id}`;
55
87
  const body = doc;
56
- const resp = await (0, import_needle.default)("put", url, body, opts);
88
+ logger.info(`Putting document with id: ${doc._id}`);
89
+ let resp;
90
+ try {
91
+ resp = await (0, import_needle.default)("put", url, body, opts);
92
+ } catch (err) {
93
+ logger.error("Error during put operation:", err);
94
+ import_errors.RetryableError.handleNetworkError(err);
95
+ }
96
+ if (!resp) {
97
+ logger.error("No response received from put request");
98
+ throw new import_errors.RetryableError("no response", 503);
99
+ }
57
100
  const result = resp?.body || {};
58
101
  result.statusCode = resp.statusCode;
59
102
  if (resp.statusCode === 409) {
103
+ logger.warn(`Conflict detected for document: ${doc._id}`);
60
104
  result.ok = false;
61
105
  result.error = "conflict";
106
+ return result;
107
+ }
108
+ if (import_errors.RetryableError.isRetryableStatusCode(resp.statusCode)) {
109
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
110
+ throw new import_errors.RetryableError(result.reason || "retryable error", resp.statusCode);
62
111
  }
112
+ logger.info(`Successfully saved document: ${doc._id}`);
63
113
  return result;
64
114
  });
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var errors_exports = {};
20
+ __export(errors_exports, {
21
+ RetryableError: () => RetryableError
22
+ });
23
+ module.exports = __toCommonJS(errors_exports);
24
+ class RetryableError extends Error {
25
+ /**
26
+ * @param {string} message - The error message
27
+ * @param {number|undefined} statusCode - The HTTP status code
28
+ */
29
+ constructor(message, statusCode) {
30
+ super(message);
31
+ this.name = "RetryableError";
32
+ this.statusCode = statusCode;
33
+ }
34
+ /**
35
+ * @param {number|undefined} statusCode - The HTTP status code to check
36
+ * @returns {boolean} Whether the status code is retryable
37
+ */
38
+ static isRetryableStatusCode(statusCode) {
39
+ if (statusCode === void 0) return false;
40
+ return [408, 429, 500, 502, 503, 504].includes(statusCode);
41
+ }
42
+ /**
43
+ * @param {NetworkError | unknown} err - The network error to handle
44
+ * @throws {RetryableError} If the error is retryable
45
+ * @throws {Error} If the error is not retryable
46
+ */
47
+ static handleNetworkError(err) {
48
+ const networkErrors = {
49
+ ECONNREFUSED: 503,
50
+ ECONNRESET: 503,
51
+ ETIMEDOUT: 503,
52
+ ENETUNREACH: 503,
53
+ ENOTFOUND: 503,
54
+ EPIPE: 503,
55
+ EHOSTUNREACH: 503,
56
+ ESOCKETTIMEDOUT: 503
57
+ };
58
+ if (typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && networkErrors[err.code]) {
59
+ throw new RetryableError(`Network error: ${err.code}`, networkErrors[err.code]);
60
+ }
61
+ throw err;
62
+ }
63
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var logger_exports = {};
20
+ __export(logger_exports, {
21
+ createLogger: () => createLogger
22
+ });
23
+ module.exports = __toCommonJS(logger_exports);
24
+ function createLogger(config) {
25
+ if (config._normalizedLogger) {
26
+ return config._normalizedLogger;
27
+ }
28
+ if (!config.logger) {
29
+ config._normalizedLogger = {
30
+ error: () => {
31
+ },
32
+ warn: () => {
33
+ },
34
+ info: () => {
35
+ },
36
+ debug: () => {
37
+ }
38
+ };
39
+ return config._normalizedLogger;
40
+ }
41
+ if (typeof config.logger === "function") {
42
+ config._normalizedLogger = {
43
+ error: (...args) => config.logger("error", ...args),
44
+ warn: (...args) => config.logger("warn", ...args),
45
+ info: (...args) => config.logger("info", ...args),
46
+ debug: (...args) => config.logger("debug", ...args)
47
+ };
48
+ return config._normalizedLogger;
49
+ }
50
+ config._normalizedLogger = {
51
+ error: config.logger.error || (() => {
52
+ }),
53
+ warn: config.logger.warn || (() => {
54
+ }),
55
+ info: config.logger.info || (() => {
56
+ }),
57
+ debug: config.logger.debug || (() => {
58
+ })
59
+ };
60
+ return config._normalizedLogger;
61
+ }
@@ -18,19 +18,21 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var patch_exports = {};
20
20
  __export(patch_exports, {
21
- patch: () => patch
21
+ patch: () => patch,
22
+ sleep: () => sleep
22
23
  });
23
24
  module.exports = __toCommonJS(patch_exports);
24
25
  var import_crud = require("./crud.cjs");
25
26
  var import_patch = require("../schema/patch.cjs");
26
27
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27
- const patch = import_patch.CouchPatch.implement(async (config, id, properties) => {
28
- const maxRetries = config.retries || 5;
29
- const delay = config.delay || 1e3;
28
+ const patch = import_patch.Patch.implement(async (config, id, properties) => {
29
+ const maxRetries = config.maxRetries || 5;
30
+ let delay = config.initialDelay || 1e3;
30
31
  let attempts = 0;
31
32
  while (attempts <= maxRetries) {
32
33
  try {
33
34
  const doc = await (0, import_crud.get)(config, id);
35
+ if (!doc) return { ok: false, statusCode: 404, error: "not_found" };
34
36
  const updatedDoc = { ...doc, ...properties };
35
37
  const result = await (0, import_crud.put)(config, updatedDoc);
36
38
  if (result.ok) {
@@ -41,10 +43,13 @@ const patch = import_patch.CouchPatch.implement(async (config, id, properties) =
41
43
  throw new Error(`Failed to patch after ${maxRetries} attempts`);
42
44
  }
43
45
  await sleep(delay);
46
+ delay *= config.backoffFactor;
44
47
  } catch (err) {
48
+ if (err.message !== "not_found") return { ok: false, statusCode: 404, error: "not_found" };
45
49
  attempts++;
46
50
  if (attempts > maxRetries) {
47
- throw new Error(`Failed to patch after ${maxRetries} attempts: ${err.message}`);
51
+ const error = `Failed to patch after ${maxRetries} attempts: ${err.message}`;
52
+ return { ok: false, statusCode: 500, error };
48
53
  }
49
54
  await sleep(delay);
50
55
  }
@@ -35,6 +35,7 @@ module.exports = __toCommonJS(query_exports);
35
35
  var import_zod = require("zod");
36
36
  var import_needle = __toESM(require("needle"), 1);
37
37
  var import_query = require("../schema/query.cjs");
38
+ var import_errors = require("./errors.cjs");
38
39
  var import_lodash = __toESM(require("lodash"), 1);
39
40
  const { includes } = import_lodash.default;
40
41
  const query = import_query.SimpleViewQuery.implement(async (config, view, options) => {
@@ -46,8 +47,17 @@ const query = import_query.SimpleViewQuery.implement(async (config, view, option
46
47
  }
47
48
  };
48
49
  const url = `${config.couch}/${view}?${qs.toString()}`;
49
- const results = await (0, import_needle.default)("get", url, opts);
50
+ let results;
51
+ try {
52
+ results = await (0, import_needle.default)("get", url, opts);
53
+ } catch (err) {
54
+ import_errors.RetryableError.handleNetworkError(err);
55
+ }
56
+ if (!results) throw new import_errors.RetryableError("no response", 503);
50
57
  const body = results.body;
58
+ if (import_errors.RetryableError.isRetryableStatusCode(results.statusCode)) {
59
+ throw new import_errors.RetryableError(body.error || "retryable error during query", results.statusCode);
60
+ }
51
61
  if (body.error) throw new Error(body.error);
52
62
  return body;
53
63
  });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var retry_exports = {};
20
+ __export(retry_exports, {
21
+ withRetry: () => withRetry
22
+ });
23
+ module.exports = __toCommonJS(retry_exports);
24
+ var import_errors = require("./errors.cjs");
25
+ var import_patch = require("./patch.cjs");
26
+ function withRetry(fn, options = {}) {
27
+ const {
28
+ maxRetries = 3,
29
+ initialDelay = 1e3,
30
+ // 1 second
31
+ backoffFactor = 2
32
+ // exponential backoff multiplier
33
+ } = options;
34
+ return async (...args) => {
35
+ let lastError;
36
+ let delay = initialDelay;
37
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
38
+ try {
39
+ return await fn(...args);
40
+ } catch (error) {
41
+ lastError = error;
42
+ if (!(error instanceof import_errors.RetryableError)) {
43
+ throw error;
44
+ }
45
+ if (attempt === maxRetries) {
46
+ throw error;
47
+ }
48
+ await (0, import_patch.sleep)(delay);
49
+ delay *= backoffFactor;
50
+ }
51
+ }
52
+ throw lastError;
53
+ };
54
+ }
@@ -33,17 +33,58 @@ __export(stream_exports, {
33
33
  module.exports = __toCommonJS(stream_exports);
34
34
  var import_needle = __toESM(require("needle"), 1);
35
35
  var import_query = require("./query.cjs");
36
+ var import_errors = require("./errors.cjs");
36
37
  var import_JSONStream = __toESM(require("JSONStream"), 1);
37
- const queryStream = (config, view, options) => new Promise((resolve, reject) => {
38
+ const queryStream = (config, view, options, onRow) => new Promise((resolve, reject) => {
38
39
  if (!options) options = {};
39
- const { onRow, ...rest } = options;
40
- const qs = (0, import_query.queryString)(rest, ["key", "startkey", "endkey", "reduce", "group", "group_level", "stale", "limit"]);
40
+ const qs = (0, import_query.queryString)(options, ["key", "startkey", "endkey", "reduce", "group", "group_level", "stale", "limit"]);
41
41
  const url = `${config.couch}/${view}?${qs.toString()}`;
42
+ const opts = {
43
+ json: true,
44
+ headers: {
45
+ "Content-Type": "application/json"
46
+ },
47
+ parse_response: false
48
+ // Keep as stream
49
+ };
42
50
  const streamer = import_JSONStream.default.parse("rows.*");
43
51
  streamer.on("data", onRow);
44
- streamer.on("end", (err) => {
45
- if (err) return reject(err);
46
- resolve(null);
52
+ streamer.on(
53
+ "error",
54
+ /** @param {Error} err */
55
+ (err) => {
56
+ reject(new Error(`Stream parsing error: ${err.message}`));
57
+ }
58
+ );
59
+ streamer.on(
60
+ "done",
61
+ /** @param {Error|null} err */
62
+ (err) => {
63
+ try {
64
+ import_errors.RetryableError.handleNetworkError(err);
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ }
69
+ );
70
+ streamer.on("end", () => {
71
+ resolve(void 0);
47
72
  });
48
- import_needle.default.get(url).pipe(streamer);
73
+ const req = import_needle.default.get(url, opts);
74
+ req.on("response", (response) => {
75
+ if (import_errors.RetryableError.isRetryableStatusCode(response.statusCode)) {
76
+ reject(new import_errors.RetryableError("retryable error during stream query", response.statusCode));
77
+ return;
78
+ }
79
+ });
80
+ req.on("error", (err) => {
81
+ try {
82
+ import_errors.RetryableError.handleNetworkError(err);
83
+ } catch (retryErr) {
84
+ reject(retryErr);
85
+ return;
86
+ }
87
+ reject(err);
88
+ });
89
+ req.pipe(streamer);
49
90
  });
package/cjs/index.cjs CHANGED
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var index_exports = {};
20
20
  __export(index_exports, {
21
+ bindConfig: () => bindConfig,
21
22
  bulkGet: () => import_bulk.bulkGet,
22
23
  bulkRemove: () => import_bulk.bulkRemove,
23
24
  bulkSave: () => import_bulk.bulkSave,
@@ -26,7 +27,8 @@ __export(index_exports, {
26
27
  put: () => import_crud.put,
27
28
  query: () => import_query.query,
28
29
  queryStream: () => import_stream.queryStream,
29
- schema: () => schema
30
+ schema: () => schema,
31
+ withRetry: () => import_retry.withRetry
30
32
  });
31
33
  module.exports = __toCommonJS(index_exports);
32
34
  var import_bulk = require("./impl/bulk.cjs");
@@ -34,21 +36,43 @@ var import_crud = require("./impl/crud.cjs");
34
36
  var import_patch = require("./impl/patch.cjs");
35
37
  var import_query = require("./impl/query.cjs");
36
38
  var import_stream = require("./impl/stream.cjs");
39
+ var import_retry = require("./impl/retry.cjs");
37
40
  var import_bulk2 = require("./schema/bulk.cjs");
38
41
  var import_config = require("./schema/config.cjs");
39
42
  var import_query2 = require("./schema/query.cjs");
43
+ var import_stream2 = require("./schema/stream.cjs");
40
44
  var import_patch2 = require("./schema/patch.cjs");
41
45
  var import_crud2 = require("./schema/crud.cjs");
46
+ var import_bind = require("./schema/bind.cjs");
42
47
  const schema = {
43
48
  CouchConfig: import_config.CouchConfig,
44
49
  SimpleViewQuery: import_query2.SimpleViewQuery,
45
50
  SimpleViewQueryResponse: import_query2.SimpleViewQueryResponse,
51
+ SimpleViewQueryStream: import_stream2.SimpleViewQueryStream,
52
+ OnRow: import_stream2.OnRow,
46
53
  BulkSave: import_bulk2.BulkSave,
47
54
  BulkGet: import_bulk2.BulkGet,
48
55
  CouchGet: import_crud2.CouchGet,
49
56
  CouchPut: import_crud2.CouchPut,
50
57
  CouchDoc: import_crud2.CouchDoc,
51
58
  CouchDocResponse: import_crud2.CouchDocResponse,
52
- PatchConfig: import_patch2.PatchConfig,
53
59
  Patch: import_patch2.Patch
54
60
  };
61
+ const bindConfig = import_bind.Bind.implement((config) => {
62
+ const retryOptions = {
63
+ maxRetries: config.maxRetries ?? 10,
64
+ initialDelay: config.initialDelay ?? 1e3,
65
+ backoffFactor: config.backoffFactor ?? 2
66
+ };
67
+ return {
68
+ get: config.bindWithRetry ? (0, import_retry.withRetry)(import_crud.get.bind(null, config), retryOptions) : import_crud.get.bind(null, config),
69
+ put: config.bindWithRetry ? (0, import_retry.withRetry)(import_crud.put.bind(null, config), retryOptions) : import_crud.put.bind(null, config),
70
+ patch: import_patch.patch.bind(null, config),
71
+ // patch not included in retry
72
+ bulkGet: config.bindWithRetry ? (0, import_retry.withRetry)(import_bulk.bulkGet.bind(null, config), retryOptions) : import_bulk.bulkGet.bind(null, config),
73
+ bulkSave: config.bindWithRetry ? (0, import_retry.withRetry)(import_bulk.bulkSave.bind(null, config), retryOptions) : import_bulk.bulkSave.bind(null, config),
74
+ bulkRemove: config.bindWithRetry ? (0, import_retry.withRetry)(import_bulk.bulkRemove.bind(null, config), retryOptions) : import_bulk.bulkRemove.bind(null, config),
75
+ query: config.bindWithRetry ? (0, import_retry.withRetry)(import_query.query.bind(null, config), retryOptions) : import_query.query.bind(null, config),
76
+ queryStream: config.bindWithRetry ? (0, import_retry.withRetry)(import_stream.queryStream.bind(null, config), retryOptions) : import_stream.queryStream.bind(null, config)
77
+ };
78
+ });