hide-a-bed 5.2.8 → 6.0.0-beta.0

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 (362) hide show
  1. package/.prettierrc +7 -0
  2. package/README.md +270 -218
  3. package/dist/cjs/index.cjs +1952 -0
  4. package/dist/esm/index.mjs +1898 -0
  5. package/docs/.nojekyll +1 -0
  6. package/docs/assets/hierarchy.js +1 -0
  7. package/docs/assets/highlight.css +113 -0
  8. package/docs/assets/icons.js +18 -0
  9. package/docs/assets/icons.svg +1 -0
  10. package/docs/assets/main.js +60 -0
  11. package/docs/assets/navigation.js +1 -0
  12. package/docs/assets/search.js +1 -0
  13. package/docs/assets/style.css +1633 -0
  14. package/docs/classes/QueryBuilder.html +42 -0
  15. package/docs/functions/bindConfig.html +4 -0
  16. package/docs/functions/bulkGet.html +14 -0
  17. package/docs/functions/bulkGetDictionary.html +10 -0
  18. package/docs/functions/bulkRemove.html +12 -0
  19. package/docs/functions/bulkRemoveMap.html +11 -0
  20. package/docs/functions/bulkSave.html +10 -0
  21. package/docs/functions/bulkSaveTransaction.html +23 -0
  22. package/docs/functions/createLock.html +7 -0
  23. package/docs/functions/createQuery.html +1 -0
  24. package/docs/functions/get.html +1 -0
  25. package/docs/functions/getAtRev.html +1 -0
  26. package/docs/functions/getDBInfo.html +10 -0
  27. package/docs/functions/patch.html +8 -0
  28. package/docs/functions/patchDangerously.html +9 -0
  29. package/docs/functions/put.html +1 -0
  30. package/docs/functions/query.html +15 -0
  31. package/docs/functions/queryStream.html +6 -0
  32. package/docs/functions/remove.html +1 -0
  33. package/docs/functions/removeLock.html +6 -0
  34. package/docs/functions/watchDocs.html +9 -0
  35. package/docs/functions/withRetry.html +6 -0
  36. package/docs/hierarchy.html +1 -0
  37. package/docs/index.html +483 -0
  38. package/docs/interfaces/NetworkError.html +6 -0
  39. package/docs/interfaces/NotFoundError.html +10 -0
  40. package/docs/interfaces/RetryOptions.html +10 -0
  41. package/docs/interfaces/RetryableError.html +10 -0
  42. package/docs/interfaces/StandardSchemaV1.FailureResult.html +4 -0
  43. package/docs/interfaces/StandardSchemaV1.Issue.html +6 -0
  44. package/docs/interfaces/StandardSchemaV1.Options.html +3 -0
  45. package/docs/interfaces/StandardSchemaV1.PathSegment.html +4 -0
  46. package/docs/interfaces/StandardSchemaV1.Props.html +10 -0
  47. package/docs/interfaces/StandardSchemaV1.SuccessResult.html +6 -0
  48. package/docs/interfaces/StandardSchemaV1.Types.html +6 -0
  49. package/docs/interfaces/StandardSchemaV1.html +4 -0
  50. package/docs/modules/StandardSchemaV1.html +1 -0
  51. package/docs/modules.html +1 -0
  52. package/docs/types/BoundInstance.html +1 -0
  53. package/docs/types/BulkGetBound.html +2 -0
  54. package/docs/types/BulkGetDictionaryBound.html +1 -0
  55. package/docs/types/BulkGetDictionaryOptions.html +2 -0
  56. package/docs/types/BulkGetDictionaryResult.html +3 -0
  57. package/docs/types/BulkGetOptions.html +3 -0
  58. package/docs/types/BulkGetResponse.html +1 -0
  59. package/docs/types/CouchConfig-1.html +1 -0
  60. package/docs/types/CouchConfig.html +1 -0
  61. package/docs/types/CouchConfigInput.html +1 -0
  62. package/docs/types/CouchDoc-1.html +1 -0
  63. package/docs/types/CouchDoc.html +2 -0
  64. package/docs/types/CouchDocInput.html +2 -0
  65. package/docs/types/GetAtRevBound.html +1 -0
  66. package/docs/types/GetBound.html +1 -0
  67. package/docs/types/GetOptions.html +2 -0
  68. package/docs/types/LockDoc-1.html +1 -0
  69. package/docs/types/LockDoc.html +1 -0
  70. package/docs/types/LockOptions-1.html +1 -0
  71. package/docs/types/LockOptions.html +1 -0
  72. package/docs/types/LockOptionsInput.html +1 -0
  73. package/docs/types/OnInvalidDocAction.html +1 -0
  74. package/docs/types/OnRow.html +1 -0
  75. package/docs/types/QueryBound.html +1 -0
  76. package/docs/types/SimpleViewOptions-1.html +1 -0
  77. package/docs/types/SimpleViewOptions.html +1 -0
  78. package/docs/types/StandardSchemaV1.InferInput.html +2 -0
  79. package/docs/types/StandardSchemaV1.InferOutput.html +2 -0
  80. package/docs/types/StandardSchemaV1.Result.html +2 -0
  81. package/docs/types/ViewQueryResponse-1.html +1 -0
  82. package/docs/types/ViewQueryResponse.html +2 -0
  83. package/docs/types/ViewQueryResponseValidated.html +2 -0
  84. package/docs/types/ViewRow-1.html +1 -0
  85. package/docs/types/ViewRow.html +2 -0
  86. package/docs/types/ViewRowValidated.html +7 -0
  87. package/docs/types/ViewString.html +1 -0
  88. package/docs/types/WatchOptionsInput.html +1 -0
  89. package/docs/types/WatchOptionsSchema-1.html +1 -0
  90. package/docs/types/WatchOptionsSchema.html +1 -0
  91. package/eslint.config.js +15 -0
  92. package/impl/bindConfig.mts +140 -0
  93. package/impl/bulkGet.mts +256 -0
  94. package/impl/bulkGet.test.mts +159 -0
  95. package/impl/bulkRemove.mts +98 -0
  96. package/impl/bulkRemove.test.mts +102 -0
  97. package/impl/bulkSave.mts +286 -0
  98. package/impl/bulkSave.test.mts +319 -0
  99. package/impl/get.mts +137 -0
  100. package/impl/get.test.mts +114 -0
  101. package/impl/getDBInfo.mts +67 -0
  102. package/impl/getDBInfo.test.mts +62 -0
  103. package/impl/patch.mts +134 -0
  104. package/impl/patch.test.mts +142 -0
  105. package/impl/put.mts +56 -0
  106. package/impl/put.test.mts +114 -0
  107. package/impl/query.mts +224 -0
  108. package/impl/query.test.mts +280 -0
  109. package/impl/remove.mts +65 -0
  110. package/impl/remove.test.mts +82 -0
  111. package/impl/retry.mts +66 -0
  112. package/impl/retry.test.mts +77 -0
  113. package/impl/stream.mts +143 -0
  114. package/impl/stream.test.mts +205 -0
  115. package/impl/sugar/lock.mts +103 -0
  116. package/impl/sugar/lock.test.mts +113 -0
  117. package/impl/sugar/{watch.mjs → watch.mts} +56 -22
  118. package/impl/sugar/watch.test.mts +155 -0
  119. package/impl/utils/errors.mts +130 -0
  120. package/impl/utils/errors.test.mts +58 -0
  121. package/impl/utils/logger.mts +62 -0
  122. package/impl/utils/logger.test.mts +129 -0
  123. package/impl/utils/mergeNeedleOpts.mts +16 -0
  124. package/impl/utils/parseRows.mts +117 -0
  125. package/impl/utils/parseRows.test.mts +183 -0
  126. package/impl/utils/queryBuilder.mts +173 -0
  127. package/impl/utils/queryBuilder.test.mts +83 -0
  128. package/impl/utils/queryString.mts +44 -0
  129. package/impl/utils/queryString.test.mts +53 -0
  130. package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
  131. package/impl/utils/transactionErrors.mts +71 -0
  132. package/index.mts +82 -0
  133. package/index.test.mts +415 -0
  134. package/package.json +46 -32
  135. package/schema/config.mts +81 -0
  136. package/schema/couch/couch.input.schema.ts +43 -0
  137. package/schema/couch/couch.output.schema.ts +169 -0
  138. package/schema/sugar/lock.mts +18 -0
  139. package/schema/sugar/watch.mts +14 -0
  140. package/schema/util.mts +8 -0
  141. package/tsconfig.json +10 -4
  142. package/tsdown.config.ts +16 -0
  143. package/typedoc.json +4 -0
  144. package/types/output/eslint.config.d.ts +3 -0
  145. package/types/output/eslint.config.d.ts.map +1 -0
  146. package/types/output/impl/bindConfig.d.mts +174 -0
  147. package/types/output/impl/bindConfig.d.mts.map +1 -0
  148. package/types/output/impl/bulkGet.d.mts +75 -0
  149. package/types/output/impl/bulkGet.d.mts.map +1 -0
  150. package/types/output/impl/bulkGet.test.d.mts +2 -0
  151. package/types/output/impl/bulkGet.test.d.mts.map +1 -0
  152. package/types/output/impl/bulkRemove.d.mts +63 -0
  153. package/types/output/impl/bulkRemove.d.mts.map +1 -0
  154. package/types/output/impl/bulkRemove.test.d.mts +2 -0
  155. package/types/output/impl/bulkRemove.test.d.mts.map +1 -0
  156. package/types/output/impl/bulkSave.d.mts +64 -0
  157. package/types/output/impl/bulkSave.d.mts.map +1 -0
  158. package/types/output/impl/bulkSave.test.d.mts +2 -0
  159. package/types/output/impl/bulkSave.test.d.mts.map +1 -0
  160. package/types/output/impl/get.d.mts +20 -0
  161. package/types/output/impl/get.d.mts.map +1 -0
  162. package/types/output/impl/get.test.d.mts +2 -0
  163. package/types/output/impl/get.test.d.mts.map +1 -0
  164. package/types/output/impl/getDBInfo.d.mts +52 -0
  165. package/types/output/impl/getDBInfo.d.mts.map +1 -0
  166. package/types/output/impl/getDBInfo.test.d.mts +2 -0
  167. package/types/output/impl/getDBInfo.test.d.mts.map +1 -0
  168. package/types/output/impl/patch.d.mts +45 -0
  169. package/types/output/impl/patch.d.mts.map +1 -0
  170. package/types/output/impl/patch.test.d.mts +2 -0
  171. package/types/output/impl/patch.test.d.mts.map +1 -0
  172. package/types/output/impl/put.d.mts +5 -0
  173. package/types/output/impl/put.d.mts.map +1 -0
  174. package/types/output/impl/put.test.d.mts +2 -0
  175. package/types/output/impl/put.test.d.mts.map +1 -0
  176. package/types/output/impl/query.d.mts +47 -0
  177. package/types/output/impl/query.d.mts.map +1 -0
  178. package/types/output/impl/query.test.d.mts +2 -0
  179. package/types/output/impl/query.test.d.mts.map +1 -0
  180. package/types/output/impl/remove.d.mts +9 -0
  181. package/types/output/impl/remove.d.mts.map +1 -0
  182. package/types/output/impl/remove.test.d.mts +2 -0
  183. package/types/output/impl/remove.test.d.mts.map +1 -0
  184. package/types/output/impl/retry.d.mts +32 -0
  185. package/types/output/impl/retry.d.mts.map +1 -0
  186. package/types/output/impl/retry.test.d.mts +2 -0
  187. package/types/output/impl/retry.test.d.mts.map +1 -0
  188. package/types/output/impl/stream.d.mts +13 -0
  189. package/types/output/impl/stream.d.mts.map +1 -0
  190. package/types/output/impl/stream.test.d.mts +2 -0
  191. package/types/output/impl/stream.test.d.mts.map +1 -0
  192. package/types/output/impl/sugar/lock.d.mts +24 -0
  193. package/types/output/impl/sugar/lock.d.mts.map +1 -0
  194. package/types/output/impl/sugar/lock.test.d.mts +2 -0
  195. package/types/output/impl/sugar/lock.test.d.mts.map +1 -0
  196. package/types/output/impl/sugar/watch.d.mts +21 -0
  197. package/types/output/impl/sugar/watch.d.mts.map +1 -0
  198. package/types/output/impl/sugar/watch.test.d.mts +2 -0
  199. package/types/output/impl/sugar/watch.test.d.mts.map +1 -0
  200. package/types/output/impl/utils/errors.d.mts +78 -0
  201. package/types/output/impl/utils/errors.d.mts.map +1 -0
  202. package/types/output/impl/utils/errors.test.d.mts +2 -0
  203. package/types/output/impl/utils/errors.test.d.mts.map +1 -0
  204. package/types/output/impl/utils/logger.d.mts +11 -0
  205. package/types/output/impl/utils/logger.d.mts.map +1 -0
  206. package/types/output/impl/utils/logger.test.d.mts +2 -0
  207. package/types/output/impl/utils/logger.test.d.mts.map +1 -0
  208. package/types/output/impl/utils/mergeNeedleOpts.d.mts +53 -0
  209. package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +1 -0
  210. package/types/output/impl/utils/parseRows.d.mts +15 -0
  211. package/types/output/impl/utils/parseRows.d.mts.map +1 -0
  212. package/types/output/impl/utils/parseRows.test.d.mts +2 -0
  213. package/types/output/impl/utils/parseRows.test.d.mts.map +1 -0
  214. package/types/output/impl/utils/queryBuilder.d.mts +68 -0
  215. package/types/output/impl/utils/queryBuilder.d.mts.map +1 -0
  216. package/types/output/impl/utils/queryBuilder.test.d.mts +2 -0
  217. package/types/output/impl/utils/queryBuilder.test.d.mts.map +1 -0
  218. package/types/output/impl/utils/queryString.d.mts +9 -0
  219. package/types/output/impl/utils/queryString.d.mts.map +1 -0
  220. package/types/output/impl/utils/queryString.test.d.mts +2 -0
  221. package/types/output/impl/utils/queryString.test.d.mts.map +1 -0
  222. package/types/output/impl/utils/trackedEmitter.d.mts +7 -0
  223. package/types/output/impl/utils/trackedEmitter.d.mts.map +1 -0
  224. package/{impl → types/output/impl/utils}/transactionErrors.d.mts +16 -31
  225. package/types/output/impl/utils/transactionErrors.d.mts.map +1 -0
  226. package/types/output/index.d.mts +32 -0
  227. package/types/output/index.d.mts.map +1 -0
  228. package/types/output/index.test.d.mts +2 -0
  229. package/types/output/index.test.d.mts.map +1 -0
  230. package/types/output/schema/config.d.mts +90 -0
  231. package/types/output/schema/config.d.mts.map +1 -0
  232. package/types/output/schema/couch/couch.input.schema.d.ts +29 -0
  233. package/types/output/schema/couch/couch.input.schema.d.ts.map +1 -0
  234. package/types/output/schema/couch/couch.output.schema.d.ts +113 -0
  235. package/types/output/schema/couch/couch.output.schema.d.ts.map +1 -0
  236. package/types/output/schema/sugar/lock.d.mts +19 -0
  237. package/types/output/schema/sugar/lock.d.mts.map +1 -0
  238. package/types/output/schema/sugar/watch.d.mts +11 -0
  239. package/types/output/schema/sugar/watch.d.mts.map +1 -0
  240. package/types/output/schema/util.d.mts +85 -0
  241. package/types/output/schema/util.d.mts.map +1 -0
  242. package/types/output/tsdown.config.d.ts +3 -0
  243. package/types/output/tsdown.config.d.ts.map +1 -0
  244. package/types/output/types/standard-schema.d.ts +60 -0
  245. package/types/output/types/standard-schema.d.ts.map +1 -0
  246. package/types/standard-schema.ts +76 -0
  247. package/types/utils.d.ts +1 -0
  248. package/cjs/impl/bulk.cjs +0 -275
  249. package/cjs/impl/changes.cjs +0 -67
  250. package/cjs/impl/crud.cjs +0 -127
  251. package/cjs/impl/errors.cjs +0 -75
  252. package/cjs/impl/logger.cjs +0 -70
  253. package/cjs/impl/patch.cjs +0 -95
  254. package/cjs/impl/query.cjs +0 -116
  255. package/cjs/impl/queryBuilder.cjs +0 -163
  256. package/cjs/impl/retry.cjs +0 -55
  257. package/cjs/impl/stream.cjs +0 -121
  258. package/cjs/impl/sugar/lock.cjs +0 -81
  259. package/cjs/impl/sugar/watch.cjs +0 -159
  260. package/cjs/impl/trackedEmitter.cjs +0 -54
  261. package/cjs/impl/transactionErrors.cjs +0 -70
  262. package/cjs/impl/util.cjs +0 -64
  263. package/cjs/index.cjs +0 -132
  264. package/cjs/integration/changes.cjs +0 -76
  265. package/cjs/integration/disconnect-watch.cjs +0 -52
  266. package/cjs/integration/watch.cjs +0 -59
  267. package/cjs/schema/bind.cjs +0 -59
  268. package/cjs/schema/bulk.cjs +0 -92
  269. package/cjs/schema/changes.cjs +0 -68
  270. package/cjs/schema/config.cjs +0 -48
  271. package/cjs/schema/crud.cjs +0 -77
  272. package/cjs/schema/patch.cjs +0 -53
  273. package/cjs/schema/query.cjs +0 -62
  274. package/cjs/schema/stream.cjs +0 -42
  275. package/cjs/schema/sugar/lock.cjs +0 -59
  276. package/cjs/schema/sugar/watch.cjs +0 -42
  277. package/cjs/schema/util.cjs +0 -39
  278. package/config.json +0 -5
  279. package/docs/compiler.png +0 -0
  280. package/dualmode.config.json +0 -11
  281. package/impl/bulk.d.mts +0 -11
  282. package/impl/bulk.d.mts.map +0 -1
  283. package/impl/bulk.mjs +0 -291
  284. package/impl/changes.d.mts +0 -12
  285. package/impl/changes.d.mts.map +0 -1
  286. package/impl/changes.mjs +0 -53
  287. package/impl/crud.d.mts +0 -7
  288. package/impl/crud.d.mts.map +0 -1
  289. package/impl/crud.mjs +0 -108
  290. package/impl/errors.d.mts +0 -43
  291. package/impl/errors.d.mts.map +0 -1
  292. package/impl/errors.mjs +0 -65
  293. package/impl/logger.d.mts +0 -32
  294. package/impl/logger.d.mts.map +0 -1
  295. package/impl/logger.mjs +0 -59
  296. package/impl/patch.d.mts +0 -6
  297. package/impl/patch.d.mts.map +0 -1
  298. package/impl/patch.mjs +0 -88
  299. package/impl/query.d.mts +0 -195
  300. package/impl/query.d.mts.map +0 -1
  301. package/impl/query.mjs +0 -122
  302. package/impl/queryBuilder.d.mts +0 -154
  303. package/impl/queryBuilder.d.mts.map +0 -1
  304. package/impl/queryBuilder.mjs +0 -175
  305. package/impl/retry.d.mts +0 -2
  306. package/impl/retry.d.mts.map +0 -1
  307. package/impl/retry.mjs +0 -39
  308. package/impl/stream.d.mts +0 -3
  309. package/impl/stream.d.mts.map +0 -1
  310. package/impl/stream.mjs +0 -98
  311. package/impl/sugar/lock.d.mts +0 -5
  312. package/impl/sugar/lock.d.mts.map +0 -1
  313. package/impl/sugar/lock.mjs +0 -70
  314. package/impl/sugar/watch.d.mts +0 -34
  315. package/impl/sugar/watch.d.mts.map +0 -1
  316. package/impl/trackedEmitter.d.mts +0 -8
  317. package/impl/trackedEmitter.d.mts.map +0 -1
  318. package/impl/transactionErrors.d.mts.map +0 -1
  319. package/impl/transactionErrors.mjs +0 -47
  320. package/impl/util.d.mts +0 -3
  321. package/impl/util.d.mts.map +0 -1
  322. package/impl/util.mjs +0 -35
  323. package/index.d.mts +0 -80
  324. package/index.d.mts.map +0 -1
  325. package/index.mjs +0 -141
  326. package/integration/changes.mjs +0 -60
  327. package/integration/disconnect-watch.mjs +0 -36
  328. package/integration/watch.mjs +0 -40
  329. package/schema/bind.d.mts +0 -5461
  330. package/schema/bind.d.mts.map +0 -1
  331. package/schema/bind.mjs +0 -43
  332. package/schema/bulk.d.mts +0 -923
  333. package/schema/bulk.d.mts.map +0 -1
  334. package/schema/bulk.mjs +0 -83
  335. package/schema/changes.d.mts +0 -191
  336. package/schema/changes.d.mts.map +0 -1
  337. package/schema/changes.mjs +0 -59
  338. package/schema/config.d.mts +0 -79
  339. package/schema/config.d.mts.map +0 -1
  340. package/schema/config.mjs +0 -26
  341. package/schema/crud.d.mts +0 -491
  342. package/schema/crud.d.mts.map +0 -1
  343. package/schema/crud.mjs +0 -64
  344. package/schema/patch.d.mts +0 -255
  345. package/schema/patch.d.mts.map +0 -1
  346. package/schema/patch.mjs +0 -42
  347. package/schema/query.d.mts +0 -406
  348. package/schema/query.d.mts.map +0 -1
  349. package/schema/query.mjs +0 -45
  350. package/schema/stream.d.mts +0 -211
  351. package/schema/stream.d.mts.map +0 -1
  352. package/schema/stream.mjs +0 -23
  353. package/schema/sugar/lock.d.mts +0 -238
  354. package/schema/sugar/lock.d.mts.map +0 -1
  355. package/schema/sugar/lock.mjs +0 -50
  356. package/schema/sugar/watch.d.mts +0 -127
  357. package/schema/sugar/watch.d.mts.map +0 -1
  358. package/schema/sugar/watch.mjs +0 -29
  359. package/schema/util.d.mts +0 -160
  360. package/schema/util.d.mts.map +0 -1
  361. package/schema/util.mjs +0 -35
  362. package/types/changes-stream.d.ts +0 -11
@@ -0,0 +1,1898 @@
1
+ import z, { z as z$1 } from "zod";
2
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
3
+ import needle from "needle";
4
+ import Chain from "stream-chain";
5
+ import Parser from "stream-json/Parser.js";
6
+ import Pick from "stream-json/filters/Pick.js";
7
+ import StreamArray from "stream-json/streamers/StreamArray.js";
8
+ import { EventEmitter } from "events";
9
+
10
+ //#region impl/utils/queryBuilder.mts
11
+ /**
12
+ * A builder class for constructing CouchDB view query options.
13
+ * Provides a fluent API for setting various query parameters.
14
+ * @example
15
+ * const queryOptions = new QueryBuilder()
16
+ * .limit(10)
17
+ * .include_docs()
18
+ * .startKey('someKey')
19
+ * .build();
20
+ * @see SimpleViewOptions for the full list of options.
21
+ *
22
+ * @remarks
23
+ * Each method corresponds to a CouchDB view option and returns the builder instance for chaining.
24
+ *
25
+ * @returns The constructed SimpleViewOptions object.
26
+ */
27
+ var QueryBuilder = class {
28
+ #options = {};
29
+ descending(descending = true) {
30
+ this.#options.descending = descending;
31
+ return this;
32
+ }
33
+ endkey_docid(endkeyDocId) {
34
+ this.#options.endkey_docid = endkeyDocId;
35
+ return this;
36
+ }
37
+ /**
38
+ * Alias for endkey_docid
39
+ */
40
+ end_key_doc_id(endkeyDocId) {
41
+ this.#options.endkey_docid = endkeyDocId;
42
+ return this;
43
+ }
44
+ endkey(endkey) {
45
+ this.#options.endkey = endkey;
46
+ return this;
47
+ }
48
+ /**
49
+ * Alias for endkey
50
+ */
51
+ endKey(endkey) {
52
+ this.#options.endkey = endkey;
53
+ return this;
54
+ }
55
+ /**
56
+ * Alias for endkey
57
+ */
58
+ end_key(endkey) {
59
+ this.#options.endkey = endkey;
60
+ return this;
61
+ }
62
+ group(group = true) {
63
+ this.#options.group = group;
64
+ return this;
65
+ }
66
+ group_level(level) {
67
+ this.#options.group_level = level;
68
+ return this;
69
+ }
70
+ include_docs(includeDocs = true) {
71
+ this.#options.include_docs = includeDocs;
72
+ return this;
73
+ }
74
+ inclusive_end(inclusiveEnd = true) {
75
+ this.#options.inclusive_end = inclusiveEnd;
76
+ return this;
77
+ }
78
+ key(key) {
79
+ this.#options.key = key;
80
+ return this;
81
+ }
82
+ keys(keys) {
83
+ this.#options.keys = keys;
84
+ return this;
85
+ }
86
+ limit(limit) {
87
+ this.#options.limit = limit;
88
+ return this;
89
+ }
90
+ reduce(reduce = true) {
91
+ this.#options.reduce = reduce;
92
+ return this;
93
+ }
94
+ skip(skip) {
95
+ this.#options.skip = skip;
96
+ return this;
97
+ }
98
+ sorted(sorted = true) {
99
+ this.#options.sorted = sorted;
100
+ return this;
101
+ }
102
+ stable(stable = true) {
103
+ this.#options.stable = stable;
104
+ return this;
105
+ }
106
+ startkey(startkey) {
107
+ this.#options.startkey = startkey;
108
+ return this;
109
+ }
110
+ /**
111
+ * Alias for startkey
112
+ */
113
+ startKey(startkey) {
114
+ this.#options.startkey = startkey;
115
+ return this;
116
+ }
117
+ /**
118
+ * Alias for startkey
119
+ */
120
+ start_key(startkey) {
121
+ this.#options.startkey = startkey;
122
+ return this;
123
+ }
124
+ startkey_docid(startkeyDocId) {
125
+ this.#options.startkey_docid = startkeyDocId;
126
+ return this;
127
+ }
128
+ /**
129
+ * Alias for startkey_docid
130
+ */
131
+ start_key_doc_id(startkeyDocId) {
132
+ this.#options.startkey_docid = startkeyDocId;
133
+ return this;
134
+ }
135
+ update(update) {
136
+ this.#options.update = update;
137
+ return this;
138
+ }
139
+ update_seq(updateSeq = true) {
140
+ this.#options.update_seq = updateSeq;
141
+ return this;
142
+ }
143
+ /**
144
+ * Builds and returns the ViewOptions object.
145
+ */
146
+ build() {
147
+ return { ...this.#options };
148
+ }
149
+ };
150
+ const createQuery = () => new QueryBuilder();
151
+
152
+ //#endregion
153
+ //#region schema/config.mts
154
+ const anyArgs = z$1.array(z$1.any());
155
+ const LoggerSchema = z$1.object({
156
+ error: z$1.function({
157
+ input: anyArgs,
158
+ output: z$1.void()
159
+ }).optional(),
160
+ warn: z$1.function({
161
+ input: anyArgs,
162
+ output: z$1.void()
163
+ }).optional(),
164
+ info: z$1.function({
165
+ input: anyArgs,
166
+ output: z$1.void()
167
+ }).optional(),
168
+ debug: z$1.function({
169
+ input: anyArgs,
170
+ output: z$1.void()
171
+ }).optional()
172
+ }).or(z$1.function({
173
+ input: anyArgs,
174
+ output: z$1.void()
175
+ }));
176
+ const NeedleBaseOptions = z$1.object({
177
+ json: z$1.boolean(),
178
+ headers: z$1.record(z$1.string(), z$1.string()),
179
+ parse_response: z$1.boolean().optional()
180
+ });
181
+ const NeedleOptions = z$1.object({
182
+ json: z$1.boolean().optional(),
183
+ compressed: z$1.boolean().optional(),
184
+ follow_max: z$1.number().optional(),
185
+ follow_set_cookie: z$1.boolean().optional(),
186
+ follow_set_referer: z$1.boolean().optional(),
187
+ follow: z$1.number().optional(),
188
+ timeout: z$1.number().optional(),
189
+ read_timeout: z$1.number().optional(),
190
+ parse_response: z$1.boolean().optional(),
191
+ decode: z$1.boolean().optional(),
192
+ parse_cookies: z$1.boolean().optional(),
193
+ cookies: z$1.record(z$1.string(), z$1.string()).optional(),
194
+ headers: z$1.record(z$1.string(), z$1.string()).optional(),
195
+ auth: z$1.enum([
196
+ "auto",
197
+ "digest",
198
+ "basic"
199
+ ]).optional(),
200
+ username: z$1.string().optional(),
201
+ password: z$1.string().optional(),
202
+ proxy: z$1.string().optional(),
203
+ agent: z$1.any().optional(),
204
+ rejectUnauthorized: z$1.boolean().optional(),
205
+ output: z$1.string().optional(),
206
+ parse: z$1.boolean().optional(),
207
+ multipart: z$1.boolean().optional(),
208
+ open_timeout: z$1.number().optional(),
209
+ response_timeout: z$1.number().optional(),
210
+ keepAlive: z$1.boolean().optional()
211
+ });
212
+ const CouchConfig = z$1.strictObject({
213
+ backoffFactor: z$1.number().optional().default(2).describe("multiplier for exponential backoff"),
214
+ bindWithRetry: z$1.boolean().optional().default(true).describe("should we bind with retry"),
215
+ couch: z$1.string().describe("the url of the couch db"),
216
+ initialDelay: z$1.number().optional().default(1e3).describe("initial retry delay in milliseconds"),
217
+ logger: LoggerSchema.optional().describe("logging interface supporting winston-like or simple function interface"),
218
+ maxRetries: z$1.number().optional().default(3).describe("maximum number of retry attempts"),
219
+ needleOpts: NeedleOptions.optional(),
220
+ throwOnGetNotFound: z$1.boolean().optional().default(false).describe("if a get is 404 should we throw or return undefined"),
221
+ useConsoleLogger: z$1.boolean().optional().default(false).describe("turn on console as a fallback logger"),
222
+ "~emitter": z$1.any().optional().describe("emitter for events"),
223
+ "~normalizedLogger": z$1.any().optional()
224
+ }).describe("The std config object");
225
+
226
+ //#endregion
227
+ //#region impl/utils/errors.mts
228
+ const RETRYABLE_STATUS_CODES = new Set([
229
+ 408,
230
+ 429,
231
+ 500,
232
+ 502,
233
+ 503,
234
+ 504
235
+ ]);
236
+ const NETWORK_ERROR_STATUS_MAP = {
237
+ ECONNREFUSED: 503,
238
+ ECONNRESET: 503,
239
+ ETIMEDOUT: 503,
240
+ ENETUNREACH: 503,
241
+ ENOTFOUND: 503,
242
+ EPIPE: 503,
243
+ EHOSTUNREACH: 503,
244
+ ESOCKETTIMEDOUT: 503
245
+ };
246
+ const isNetworkError = (value) => {
247
+ if (typeof value !== "object" || value === null) return false;
248
+ const candidate = value;
249
+ return typeof candidate.code === "string" && candidate.code in NETWORK_ERROR_STATUS_MAP;
250
+ };
251
+ /**
252
+ * Error thrown when a requested CouchDB document cannot be found.
253
+ *
254
+ * @remarks
255
+ * The `docId` property exposes the identifier that triggered the failure, which is
256
+ * helpful for logging and retry strategies.
257
+ *
258
+ * @public
259
+ */
260
+ var NotFoundError = class extends Error {
261
+ /**
262
+ * Identifier of the missing document.
263
+ */
264
+ docId;
265
+ /**
266
+ * Creates a new {@link NotFoundError} instance.
267
+ *
268
+ * @param docId - The identifier of the document that was not found.
269
+ * @param message - Optional custom error message.
270
+ */
271
+ constructor(docId, message = "Document not found") {
272
+ super(message);
273
+ this.name = "NotFoundError";
274
+ this.docId = docId;
275
+ }
276
+ };
277
+ /**
278
+ * Error signalling that an operation can be retried due to transient conditions.
279
+ *
280
+ * @remarks
281
+ * Use `RetryableError.isRetryableStatusCode` and `RetryableError.handleNetworkError`
282
+ * to detect when a failure should trigger retry logic.
283
+ *
284
+ * @public
285
+ */
286
+ var RetryableError = class RetryableError extends Error {
287
+ /**
288
+ * HTTP status code associated with the retryable failure, when available.
289
+ */
290
+ statusCode;
291
+ /**
292
+ * Creates a new {@link RetryableError} instance.
293
+ *
294
+ * @param message - Detailed description of the failure.
295
+ * @param statusCode - Optional HTTP status code corresponding to the failure.
296
+ */
297
+ constructor(message, statusCode) {
298
+ super(message);
299
+ this.name = "RetryableError";
300
+ this.statusCode = statusCode;
301
+ }
302
+ /**
303
+ * Determines whether the provided status code should be treated as retryable.
304
+ *
305
+ * @param statusCode - HTTP status code returned by CouchDB.
306
+ *
307
+ * @returns `true` if the status code is considered retryable; otherwise `false`.
308
+ */
309
+ static isRetryableStatusCode(statusCode) {
310
+ if (typeof statusCode !== "number") return false;
311
+ return RETRYABLE_STATUS_CODES.has(statusCode);
312
+ }
313
+ /**
314
+ * Converts low-level network errors into {@link RetryableError} instances when possible.
315
+ *
316
+ * @param err - The error thrown by the underlying HTTP client.
317
+ *
318
+ * @throws {@link RetryableError} When the error maps to a retryable network condition.
319
+ * @throws {*} Re-throws the original error when it cannot be mapped.
320
+ */
321
+ static handleNetworkError(err) {
322
+ if (isNetworkError(err)) {
323
+ const statusCode = NETWORK_ERROR_STATUS_MAP[err.code];
324
+ if (statusCode) throw new RetryableError(`Network error: ${err.code}`, statusCode);
325
+ }
326
+ throw err;
327
+ }
328
+ };
329
+ function isConflictError(err) {
330
+ if (typeof err !== "object" || err === null) return false;
331
+ return err.statusCode === 409;
332
+ }
333
+
334
+ //#endregion
335
+ //#region impl/retry.mts
336
+ /**
337
+ * Wrap an async-capable function with retry semantics that respect {@link RetryableError}.
338
+ * @typeParam Fn - The function signature to decorate with retry handling.
339
+ * @param fn The function to invoke with retry support.
340
+ * @param options Retry tuning parameters.
341
+ * @returns A function mirroring `fn` that automatically retries on {@link RetryableError}.
342
+ */
343
+ function withRetry(fn, options = {}) {
344
+ const { maxRetries = 3, initialDelay = 1e3, backoffFactor = 2, maxDelay = 3e4 } = options;
345
+ return async (...args) => {
346
+ let delay = initialDelay;
347
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
348
+ return await fn(...args);
349
+ } catch (error) {
350
+ if (!(error instanceof RetryableError)) throw error;
351
+ if (attempt === maxRetries) throw error;
352
+ await setTimeout$1(Math.min(delay, maxDelay));
353
+ delay *= backoffFactor;
354
+ }
355
+ throw new RetryableError("withRetry exhausted retry attempts without resolving the operation");
356
+ };
357
+ }
358
+
359
+ //#endregion
360
+ //#region impl/utils/logger.mts
361
+ const noop = () => {};
362
+ const createConsoleLogger = () => ({
363
+ error: (...args) => console.error(...args),
364
+ warn: (...args) => console.warn(...args),
365
+ info: (...args) => console.info(...args),
366
+ debug: (...args) => console.debug(...args)
367
+ });
368
+ const createNoopLogger = () => ({
369
+ error: noop,
370
+ warn: noop,
371
+ info: noop,
372
+ debug: noop
373
+ });
374
+ function createLogger(config) {
375
+ if (config["~normalizedLogger"]) return config["~normalizedLogger"];
376
+ if (!config.logger) {
377
+ const normalized$1 = config.useConsoleLogger ? createConsoleLogger() : createNoopLogger();
378
+ config["~normalizedLogger"] = normalized$1;
379
+ return normalized$1;
380
+ }
381
+ if (typeof config.logger === "function") {
382
+ const loggerFn = config.logger;
383
+ const normalized$1 = {
384
+ error: (...args) => loggerFn("error", ...args),
385
+ warn: (...args) => loggerFn("warn", ...args),
386
+ info: (...args) => loggerFn("info", ...args),
387
+ debug: (...args) => loggerFn("debug", ...args)
388
+ };
389
+ config["~normalizedLogger"] = normalized$1;
390
+ return normalized$1;
391
+ }
392
+ const loggerObj = config.logger;
393
+ const normalized = {
394
+ error: loggerObj.error ?? noop,
395
+ warn: loggerObj.warn ?? noop,
396
+ info: loggerObj.info ?? noop,
397
+ debug: loggerObj.debug ?? noop
398
+ };
399
+ config["~normalizedLogger"] = normalized;
400
+ return normalized;
401
+ }
402
+
403
+ //#endregion
404
+ //#region schema/util.mts
405
+ const MergeNeedleOpts = z$1.function({
406
+ input: [CouchConfig, NeedleBaseOptions],
407
+ output: NeedleOptions
408
+ });
409
+
410
+ //#endregion
411
+ //#region impl/utils/mergeNeedleOpts.mts
412
+ const mergeNeedleOpts = MergeNeedleOpts.implement((config, opts) => {
413
+ if (config.needleOpts) return {
414
+ ...opts,
415
+ ...config.needleOpts,
416
+ headers: {
417
+ ...opts.headers,
418
+ ...config.needleOpts.headers ?? {}
419
+ }
420
+ };
421
+ return opts;
422
+ });
423
+
424
+ //#endregion
425
+ //#region schema/couch/couch.output.schema.ts
426
+ /**
427
+ * Default schema for a returned CouchDB document if no validation schema is provided.
428
+ */
429
+ const CouchDoc = z$1.looseObject({
430
+ _id: z$1.string().describe("the couch doc id"),
431
+ _rev: z$1.string().optional().nullish().describe("the doc revision"),
432
+ _deleted: z$1.boolean().optional().describe("is the doc deleted")
433
+ });
434
+ /**
435
+ * Default schema for a CouchDB view row if no validation schema is provided.
436
+ */
437
+ const ViewRow = z$1.object({
438
+ id: z$1.string().optional(),
439
+ key: z$1.any().nullish(),
440
+ value: z$1.any().nullish(),
441
+ doc: CouchDoc.nullish(),
442
+ error: z$1.string().optional().describe("usually not_found, if something is wrong with this doc")
443
+ });
444
+ /**
445
+ * Response type for a CouchDB view query if no validation schemas are provided.
446
+ */
447
+ const ViewQueryResponse = z$1.object({
448
+ total_rows: z$1.number().nonnegative().optional().describe("total rows in the view"),
449
+ offset: z$1.number().nonnegative().optional().describe("the offset of the first row in this result set"),
450
+ error: z$1.string().optional().describe("if something is wrong"),
451
+ rows: z$1.array(ViewRow).optional().describe("the rows returned by the view"),
452
+ update_seq: z$1.number().optional().describe("the update sequence of the database at the time of the query")
453
+ });
454
+ /**
455
+ * CouchDB _bulk_docs response schema
456
+ */
457
+ const BulkSaveResponse = z$1.array(z$1.object({
458
+ ok: z$1.boolean().nullish(),
459
+ id: z$1.string().nullish(),
460
+ rev: z$1.string().nullish(),
461
+ error: z$1.string().nullish().describe("if an error occurred, one word reason, eg conflict"),
462
+ reason: z$1.string().nullish().describe("a full error message")
463
+ }));
464
+ const CouchPutResponse = z$1.object({
465
+ ok: z$1.boolean().optional().describe("did the request succeed"),
466
+ error: z$1.string().optional().describe("the error message, if did not succeed"),
467
+ statusCode: z$1.number(),
468
+ id: z$1.string().optional().describe("the couch doc id"),
469
+ rev: z$1.string().optional().describe("the new rev of the doc")
470
+ });
471
+ const CouchDBInfo = z$1.looseObject({
472
+ cluster: z$1.object({
473
+ n: z$1.number().describe("Replicas. The number of copies of every document.").optional(),
474
+ q: z$1.number().describe("Shards. The number of range partitions.").optional(),
475
+ r: z$1.number().describe("Read quorum. The number of consistent copies of a document that need to be read before a successful reply.").optional(),
476
+ w: z$1.number().describe("Write quorum. The number of copies of a document that need to be written before a successful reply.").optional()
477
+ }).optional(),
478
+ compact_running: z$1.boolean().describe("Set to true if the database compaction routine is operating on this database.").optional(),
479
+ db_name: z$1.string().describe("The name of the database."),
480
+ disk_format_version: z$1.number().describe("The version of the physical format used for the data when it is stored on disk.").optional(),
481
+ doc_count: z$1.number().describe("A count of the documents in the specified database.").optional(),
482
+ doc_del_count: z$1.number().describe("Number of deleted documents").optional(),
483
+ instance_start_time: z$1.string().optional(),
484
+ purge_seq: z$1.string().describe("An opaque string that describes the purge state of the database. Do not rely on this string for counting the number of purge operations.").optional(),
485
+ sizes: z$1.object({
486
+ active: z$1.number().describe("The size of live data inside the database, in bytes.").optional(),
487
+ external: z$1.number().describe("The uncompressed size of database contents in bytes.").optional(),
488
+ file: z$1.number().describe("The size of the database file on disk in bytes. Views indexes are not included in the calculation.").optional()
489
+ }).optional(),
490
+ update_seq: z$1.string().or(z$1.number()).describe("An opaque string that describes the state of the database. Do not rely on this string for counting the number of updates.").optional(),
491
+ props: z$1.object({ partitioned: z$1.boolean().describe("If present and true, this indicates that the database is partitioned.").optional() }).optional()
492
+ });
493
+
494
+ //#endregion
495
+ //#region impl/utils/parseRows.mts
496
+ async function parseRows(rows, options) {
497
+ if (!Array.isArray(rows)) throw new Error("invalid rows format");
498
+ const isFinalRow = (row) => row !== "skip";
499
+ return (await Promise.all(rows.map(async (row) => {
500
+ try {
501
+ /**
502
+ * If no doc is present, parse without doc validation.
503
+ * This allows handling of not-found documents or rows without docs.
504
+ */
505
+ if (row.doc == null) {
506
+ const parsedRow = z$1.looseObject(ViewRow.shape).parse(row);
507
+ if (options.keySchema) {
508
+ const parsedKey$1 = await options.keySchema["~standard"].validate(row.key);
509
+ if (parsedKey$1.issues) throw parsedKey$1.issues;
510
+ parsedRow.key = parsedKey$1.value;
511
+ }
512
+ if (options.valueSchema) {
513
+ const parsedValue$1 = await options.valueSchema["~standard"].validate(row.value);
514
+ if (parsedValue$1.issues) throw parsedValue$1.issues;
515
+ parsedRow.value = parsedValue$1.value;
516
+ }
517
+ return parsedRow;
518
+ }
519
+ let parsedDoc = row.doc;
520
+ let parsedKey = row.key;
521
+ let parsedValue = row.value;
522
+ if (options.docSchema) {
523
+ const parsedDocRes = await options.docSchema["~standard"].validate(row.doc);
524
+ if (parsedDocRes.issues) if (options.onInvalidDoc === "skip") return "skip";
525
+ else throw parsedDocRes.issues;
526
+ else parsedDoc = parsedDocRes.value;
527
+ }
528
+ if (options.keySchema) {
529
+ const parsedKeyRes = await options.keySchema["~standard"].validate(row.key);
530
+ if (parsedKeyRes.issues) throw parsedKeyRes.issues;
531
+ else parsedKey = parsedKeyRes.value;
532
+ }
533
+ if (options.valueSchema) {
534
+ const parsedValueRes = await options.valueSchema["~standard"].validate(row.value);
535
+ if (parsedValueRes.issues) throw parsedValueRes.issues;
536
+ else parsedValue = parsedValueRes.value;
537
+ }
538
+ return {
539
+ ...row,
540
+ doc: parsedDoc,
541
+ key: parsedKey,
542
+ value: parsedValue
543
+ };
544
+ } catch (e) {
545
+ if (options.onInvalidDoc === "skip") return "skip";
546
+ else throw e;
547
+ }
548
+ }))).filter(isFinalRow);
549
+ }
550
+
551
+ //#endregion
552
+ //#region impl/bulkGet.mts
553
+ /**
554
+ * Executes the bulk get operation against CouchDB.
555
+ *
556
+ * @param _config CouchDB configuration
557
+ * @param ids Array of document IDs to retrieve
558
+ * @param includeDocs Whether to include documents in the response
559
+ *
560
+ * @returns The raw response body from CouchDB
561
+ *
562
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
563
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
564
+ */
565
+ async function executeBulkGet(_config, ids, includeDocs) {
566
+ const configParseResult = CouchConfig.safeParse(_config);
567
+ const logger = createLogger(_config);
568
+ logger.info(`Starting bulk get for ${ids.length} documents`);
569
+ if (!configParseResult.success) {
570
+ logger.error("Invalid configuration provided for bulk get", configParseResult.error);
571
+ throw configParseResult.error;
572
+ }
573
+ const config = configParseResult.data;
574
+ const url = `${config.couch}/_all_docs${includeDocs ? "?include_docs=true" : ""}`;
575
+ const payload = { keys: ids };
576
+ const mergedOpts = mergeNeedleOpts(config, {
577
+ json: true,
578
+ headers: { "Content-Type": "application/json" }
579
+ });
580
+ try {
581
+ const resp = await needle("post", url, payload, mergedOpts);
582
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
583
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
584
+ throw new RetryableError("retryable error during bulk get", resp.statusCode);
585
+ }
586
+ if (resp.statusCode !== 200) {
587
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
588
+ throw new Error("could not fetch");
589
+ }
590
+ return resp.body;
591
+ } catch (err) {
592
+ logger.error("Network error during bulk get:", err);
593
+ RetryableError.handleNetworkError(err);
594
+ }
595
+ }
596
+ /**
597
+ * Bulk get documents by IDs with options.
598
+ *
599
+ * @template DocSchema - schema (StandardSchemaV1) used to validate each returned document, if provided.
600
+ *
601
+ * @param config - CouchDB configuration data that is validated before use.
602
+ * @param ids - Array of document IDs to retrieve.
603
+ * @param options - Options for bulk get operation, including whether to include documents and validation schema.
604
+ *
605
+ * @returns The bulk get response with rows optionally validated against the supplied document schema.
606
+ *
607
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
608
+ * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
609
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
610
+ */
611
+ async function _bulkGetWithOptions(config, ids, options = {}) {
612
+ const body = await executeBulkGet(config, ids, options.includeDocs ?? true);
613
+ if (!body) throw new RetryableError("no response", 503);
614
+ if (body.error) throw new Error(typeof body.reason === "string" ? body.reason : "could not fetch");
615
+ const docSchema = options.validate?.docSchema || CouchDoc;
616
+ const rows = await parseRows(body.rows, {
617
+ onInvalidDoc: options.validate?.onInvalidDoc,
618
+ docSchema
619
+ });
620
+ return {
621
+ ...body,
622
+ rows
623
+ };
624
+ }
625
+ /**
626
+ * Bulk get documents by IDs.
627
+ *
628
+ * @remarks
629
+ * By default, documents are included in the response. To exclude documents, set `includeDocs` to `false`.
630
+ * When `includeDocs` is `true`, you can provide a schema (StandardSchemaV1) to validate the documents.
631
+ * When a schema is provided, you can specify how to handle invalid documents using `onInvalidDoc` option.
632
+ * `onInvalidDoc` can be set to `'throw'` (default) to throw an error on invalid documents, or `'skip'` to omit them from the results.
633
+ *
634
+ * @template DocSchema - schema (StandardSchemaV1) used to validate each returned document, if provided.
635
+ *
636
+ * @param config - CouchDB configuration data that is validated before use.
637
+ * @param ids - Array of document IDs to retrieve.
638
+ * @param options - Options for bulk get operation, including whether to include documents and validation schema.
639
+ *
640
+ * @returns The bulk get response with rows optionally validated against the supplied document schema.
641
+ *
642
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
643
+ * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
644
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
645
+ */
646
+ async function bulkGet(config, ids, options = {}) {
647
+ return _bulkGetWithOptions(config, ids, {
648
+ includeDocs: options.includeDocs,
649
+ validate: options?.validate
650
+ });
651
+ }
652
+ /**
653
+ * Bulk get documents by IDs and return a dictionary of found and not found documents.
654
+ *
655
+ * @template DocSchema - Schema used to validate each returned document, if provided. Note: if a document is found and it fails validation this will throw a Error<StandardSchemaV1.FailureResult["issues"]>.
656
+ *
657
+ * @param config - CouchDB configuration data that is validated before use.
658
+ * @param ids - Array of document IDs to retrieve.
659
+ * @param options - Options for bulk get operation, including validation schema.
660
+ *
661
+ * @returns An object containing found documents and not found rows.
662
+ *
663
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
664
+ * @throws {Error<StandardSchemaV1.FailureResult["issues"]>} When the configuration or validation schemas fail to parse.
665
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
666
+ */
667
+ async function bulkGetDictionary(config, ids, options) {
668
+ const response = await bulkGet(config, ids, {
669
+ includeDocs: true,
670
+ ...options
671
+ });
672
+ const results = {
673
+ found: {},
674
+ notFound: {}
675
+ };
676
+ for (const row of response.rows ?? []) {
677
+ const key = typeof row.key === "string" ? row.key : row.id;
678
+ if (!key) continue;
679
+ if (row.error || !row.doc) {
680
+ results.notFound[key] = row;
681
+ continue;
682
+ }
683
+ const doc = row.doc;
684
+ const docId = typeof doc?._id === "string" ? doc._id : row.id;
685
+ if (!docId) {
686
+ results.notFound[key] = row;
687
+ continue;
688
+ }
689
+ results.found[docId] = doc;
690
+ }
691
+ return results;
692
+ }
693
+
694
+ //#endregion
695
+ //#region impl/get.mts
696
+ const ValidSchema = z$1.custom((value) => {
697
+ return value !== null && typeof value === "object" && "~standard" in value;
698
+ }, { message: "docSchema must be a valid StandardSchemaV1 schema" });
699
+ const CouchGetOptions = z$1.object({
700
+ rev: z$1.string().optional().describe("the couch doc revision"),
701
+ validate: z$1.object({ docSchema: ValidSchema.optional() }).optional().describe("optional document validation rules")
702
+ });
703
+ async function _getWithOptions(config, id, options) {
704
+ const parsedOptions = CouchGetOptions.parse({
705
+ rev: options.rev,
706
+ validate: options.validate
707
+ });
708
+ const logger = createLogger(config);
709
+ const rev = parsedOptions.rev;
710
+ const path = rev ? `${id}?rev=${rev}` : id;
711
+ const url = `${config.couch}/${path}`;
712
+ const requestOptions = mergeNeedleOpts(config, {
713
+ json: true,
714
+ headers: { "Content-Type": "application/json" }
715
+ });
716
+ logger.info(`Getting document with id: ${id}, rev ${rev ?? "latest"}`);
717
+ try {
718
+ const resp = await needle("get", url, null, requestOptions);
719
+ if (!resp) {
720
+ logger.error("No response received from get request");
721
+ throw new RetryableError("no response", 503);
722
+ }
723
+ const body = resp.body ?? null;
724
+ if (resp.statusCode === 404) {
725
+ if (config.throwOnGetNotFound) {
726
+ const reason = typeof body?.reason === "string" ? body.reason : "not_found";
727
+ logger.warn(`Document not found (throwing error): ${id}, rev ${rev ?? "latest"}`);
728
+ throw new NotFoundError(id, reason);
729
+ }
730
+ logger.debug(`Document not found (returning undefined): ${id}, rev ${rev ?? "latest"}`);
731
+ return null;
732
+ }
733
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
734
+ const reason = typeof body?.reason === "string" ? body.reason : "retryable error";
735
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
736
+ throw new RetryableError(reason, resp.statusCode);
737
+ }
738
+ if (resp.statusCode !== 200) {
739
+ const reason = typeof body?.reason === "string" ? body.reason : "failed";
740
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
741
+ throw new Error(reason);
742
+ }
743
+ const typedDoc = await (parsedOptions.validate?.docSchema ?? CouchDoc)["~standard"].validate(body);
744
+ if (typedDoc.issues) throw typedDoc.issues;
745
+ logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? "latest"}`);
746
+ return typedDoc.value;
747
+ } catch (err) {
748
+ logger.error("Error during get operation:", err);
749
+ RetryableError.handleNetworkError(err);
750
+ }
751
+ }
752
+ async function get(config, id, options) {
753
+ return _getWithOptions(config, id, options ?? {});
754
+ }
755
+ async function getAtRev(config, id, rev, options) {
756
+ return _getWithOptions(config, id, {
757
+ ...options,
758
+ rev
759
+ });
760
+ }
761
+
762
+ //#endregion
763
+ //#region schema/couch/couch.input.schema.ts
764
+ const ViewOptions = z.object({
765
+ descending: z.boolean().optional().describe("sort results descending"),
766
+ endkey_docid: z.string().optional().describe("stop returning records when this document ID is reached"),
767
+ endkey: z.any().optional(),
768
+ group_level: z.number().positive().optional().describe("group the results at this level"),
769
+ group: z.boolean().optional().describe("group the results"),
770
+ include_docs: z.boolean().optional().describe("join the id to the doc and return it"),
771
+ inclusive_end: z.boolean().optional().describe("whether the endkey is included in the result, default true"),
772
+ key: z.any().optional(),
773
+ keys: z.array(z.any()).optional(),
774
+ limit: z.number().nonnegative().optional().describe("limit the results to this many rows"),
775
+ reduce: z.boolean().optional().describe("reduce the results"),
776
+ skip: z.number().nonnegative().optional().describe("skip this many rows"),
777
+ sorted: z.boolean().optional().describe("sort returned rows, default true"),
778
+ stable: z.boolean().optional().describe("ensure the view index is not updated during the query, default false"),
779
+ startkey: z.any().optional(),
780
+ startkey_docid: z.string().optional().describe("start returning records when this document ID is reached"),
781
+ update: z.enum([
782
+ "true",
783
+ "false",
784
+ "lazy"
785
+ ]).optional().describe("whether to update the view index before returning results, default true"),
786
+ update_seq: z.boolean().optional().describe("include the update sequence in the result")
787
+ }).describe("base options for a CouchDB view query");
788
+
789
+ //#endregion
790
+ //#region impl/utils/queryString.mts
791
+ const KEYS_TO_QUOTE = [
792
+ "endkey_docid",
793
+ "endkey",
794
+ "key",
795
+ "keys",
796
+ "startkey",
797
+ "startkey_docid",
798
+ "update"
799
+ ];
800
+ /**
801
+ * Serialize CouchDB view options into a URL-safe query string, quoting values CouchDB expects as JSON.
802
+ * @param options The view options to serialize
803
+ * @param params The list of option keys that require JSON quoting
804
+ * @returns The serialized query string
805
+ */
806
+ function queryString(options = {}) {
807
+ const searchParams = new URLSearchParams();
808
+ const parsedOptions = ViewOptions.parse(options);
809
+ Object.entries(parsedOptions).forEach(([key, rawValue]) => {
810
+ let value = rawValue;
811
+ if (KEYS_TO_QUOTE.includes(key)) {
812
+ if (typeof value === "string") value = `"${value}"`;
813
+ if (Array.isArray(value)) value = "[" + value.map((i) => {
814
+ if (i === null) return "null";
815
+ if (typeof i === "string") return `"${i}"`;
816
+ if (typeof i === "object" && Object.keys(i).length === 0) return "{}";
817
+ if (typeof i === "object") return JSON.stringify(i);
818
+ return i;
819
+ }).join(",") + "]";
820
+ }
821
+ searchParams.set(key, String(value));
822
+ });
823
+ return searchParams.toString();
824
+ }
825
+
826
+ //#endregion
827
+ //#region impl/stream.mts
828
+ /**
829
+ * Execute a CouchDB view query and stream rows as they are received.
830
+ * @param rawConfig CouchDB configuration
831
+ * @param view The CouchDB view to query
832
+ * @param options Query options
833
+ * @param onRow Callback invoked for each row received
834
+ */
835
+ async function queryStream(rawConfig, view, options, onRow) {
836
+ return new Promise((resolve, reject) => {
837
+ const config = CouchConfig.parse(rawConfig);
838
+ const logger = createLogger(config);
839
+ logger.info(`Starting view query stream: ${view}`);
840
+ logger.debug("Query options:", options);
841
+ const queryOptions = options ?? {};
842
+ let method = "GET";
843
+ let payload = null;
844
+ let qs = queryString(queryOptions);
845
+ logger.debug("Generated query string:", qs);
846
+ if (typeof queryOptions.keys !== "undefined") {
847
+ const MAX_URL_LENGTH = 2e3;
848
+ const keysAsString = `keys=${encodeURIComponent(JSON.stringify(queryOptions.keys))}`;
849
+ if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) qs += (qs.length > 0 ? "&" : "") + keysAsString;
850
+ else {
851
+ method = "POST";
852
+ payload = { keys: queryOptions.keys };
853
+ }
854
+ }
855
+ const url = `${config.couch}/${view}?${qs}`;
856
+ const mergedOpts = mergeNeedleOpts(config, {
857
+ json: true,
858
+ headers: { "Content-Type": "application/json" },
859
+ parse_response: false
860
+ });
861
+ const parserPipeline = Chain.chain([
862
+ new Parser(),
863
+ new Pick({ filter: "rows" }),
864
+ new StreamArray()
865
+ ]);
866
+ let rowCount = 0;
867
+ let settled = false;
868
+ const settleReject = (err) => {
869
+ if (settled) return;
870
+ settled = true;
871
+ reject(err);
872
+ };
873
+ const settleResolve = () => {
874
+ if (settled) return;
875
+ settled = true;
876
+ resolve();
877
+ };
878
+ let request = null;
879
+ parserPipeline.on("data", (chunk) => {
880
+ try {
881
+ rowCount++;
882
+ onRow(chunk.value);
883
+ } catch (callbackErr) {
884
+ const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr));
885
+ parserPipeline.destroy(error);
886
+ settleReject(error);
887
+ }
888
+ });
889
+ parserPipeline.on("error", (err) => {
890
+ logger.error("Stream parsing error:", err);
891
+ parserPipeline.destroy();
892
+ settleReject(new Error(`Stream parsing error: ${err.message}`, { cause: err }));
893
+ });
894
+ parserPipeline.on("end", () => {
895
+ logger.info(`Stream completed, processed ${rowCount} rows`);
896
+ settleResolve();
897
+ });
898
+ request = method === "GET" ? needle.get(url, mergedOpts) : needle.post(url, payload, mergedOpts);
899
+ request.on("response", (response) => {
900
+ logger.debug(`Received response with status code: ${response.statusCode}`);
901
+ if (RetryableError.isRetryableStatusCode(response.statusCode)) {
902
+ logger.warn(`Retryable status code received: ${response.statusCode}`);
903
+ settleReject(new RetryableError("retryable error during stream query", response.statusCode));
904
+ request.destroy();
905
+ }
906
+ });
907
+ request.on("error", (err) => {
908
+ logger.error("Network error during stream query:", err);
909
+ parserPipeline.destroy(err);
910
+ try {
911
+ RetryableError.handleNetworkError(err);
912
+ } catch (retryErr) {
913
+ settleReject(retryErr);
914
+ return;
915
+ } finally {
916
+ settleReject(err);
917
+ }
918
+ });
919
+ request.pipe(parserPipeline);
920
+ });
921
+ }
922
+
923
+ //#endregion
924
+ //#region impl/put.mts
925
+ const put = async (configInput, doc) => {
926
+ const config = CouchConfig.parse(configInput);
927
+ const logger = createLogger(config);
928
+ const url = `${config.couch}/${doc._id}`;
929
+ const body = doc;
930
+ const mergedOpts = mergeNeedleOpts(config, {
931
+ json: true,
932
+ headers: { "Content-Type": "application/json" }
933
+ });
934
+ logger.info(`Putting document with id: ${doc._id}`);
935
+ let resp;
936
+ try {
937
+ resp = await needle("put", url, body, mergedOpts);
938
+ } catch (err) {
939
+ logger.error("Error during put operation:", err);
940
+ RetryableError.handleNetworkError(err);
941
+ }
942
+ if (!resp) {
943
+ logger.error("No response received from put request");
944
+ throw new RetryableError("no response", 503);
945
+ }
946
+ const result = resp?.body || {};
947
+ result.statusCode = resp.statusCode;
948
+ if (resp.statusCode === 409) {
949
+ logger.warn(`Conflict detected for document: ${doc._id}`);
950
+ result.ok = false;
951
+ result.error = "conflict";
952
+ return CouchPutResponse.parse(result);
953
+ }
954
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
955
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
956
+ throw new RetryableError(result.reason || "retryable error", resp.statusCode);
957
+ }
958
+ logger.info(`Successfully saved document: ${doc._id}`);
959
+ return CouchPutResponse.parse(result);
960
+ };
961
+
962
+ //#endregion
963
+ //#region impl/patch.mts
964
+ const PatchProperties = z$1.looseObject({ _rev: z$1.string("_rev is required for patch operations") }).describe("Patch payload with _rev");
965
+ /**
966
+ * Patch a CouchDB document by merging provided properties.
967
+ * Validates that the _rev matches before applying the patch.
968
+ *
969
+ * @param configInput - CouchDB configuration
970
+ * @param id - Document ID to patch
971
+ * @param _properties - Properties to merge into the document (must include _rev)
972
+ * @returns The result of the put operation
973
+ *
974
+ * @throws Error if the _rev does not match or other errors occur
975
+ */
976
+ const patch = async (configInput, id, _properties) => {
977
+ const config = CouchConfig.parse(configInput);
978
+ const properties = PatchProperties.parse(_properties);
979
+ const logger = createLogger(configInput);
980
+ logger.info(`Starting patch operation for document ${id}`);
981
+ logger.debug("Patch properties:", properties);
982
+ const doc = await get(config, id);
983
+ if (doc?._rev !== properties._rev) return {
984
+ statusCode: 409,
985
+ ok: false,
986
+ error: "conflict"
987
+ };
988
+ const updatedDoc = {
989
+ ...doc,
990
+ ...properties
991
+ };
992
+ logger.debug("Merged document:", updatedDoc);
993
+ const result = await put(config, updatedDoc);
994
+ logger.info(`Successfully patched document ${id}, rev: ${result.rev}`);
995
+ return result;
996
+ };
997
+ /**
998
+ * Patch a CouchDB document by merging provided properties.
999
+ * This function will retry on conflicts using an exponential backoff strategy.
1000
+ *
1001
+ * @remarks patchDangerously can clobber data. It will retry even if a conflict happens. There are some use cases for this, but you have been warned, hence the name.
1002
+ *
1003
+ * @param configInput - CouchDB configuration
1004
+ * @param id - Document ID to patch
1005
+ * @param properties - Properties to merge into the document
1006
+ * @returns The result of the put operation or an error if max retries are exceeded
1007
+ *
1008
+ * @throws Error if max retries are exceeded or other errors occur
1009
+ */
1010
+ const patchDangerously = async (configInput, id, properties) => {
1011
+ const config = CouchConfig.parse(configInput);
1012
+ const logger = createLogger(config);
1013
+ const maxRetries = config.maxRetries || 5;
1014
+ let delay = config.initialDelay || 1e3;
1015
+ let attempts = 0;
1016
+ logger.info(`Starting patch operation for document ${id}`);
1017
+ logger.debug("Patch properties:", properties);
1018
+ while (attempts <= maxRetries) {
1019
+ logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`);
1020
+ try {
1021
+ const doc = await get(config, id);
1022
+ if (!doc) {
1023
+ logger.warn(`Document ${id} not found`);
1024
+ return {
1025
+ ok: false,
1026
+ statusCode: 404,
1027
+ error: "not_found"
1028
+ };
1029
+ }
1030
+ const updatedDoc = {
1031
+ ...doc,
1032
+ ...properties
1033
+ };
1034
+ logger.debug("Merged document:", updatedDoc);
1035
+ const result = await put(config, updatedDoc);
1036
+ if (result.ok) {
1037
+ logger.info(`Successfully patched document ${id}, rev: ${result.rev}`);
1038
+ return result;
1039
+ }
1040
+ attempts++;
1041
+ if (attempts > maxRetries) {
1042
+ logger.error(`Failed to patch ${id} after ${maxRetries} attempts`);
1043
+ throw new Error(`Failed to patch after ${maxRetries} attempts`);
1044
+ }
1045
+ logger.warn(`Conflict detected for ${id}, retrying (attempt ${attempts})`);
1046
+ await setTimeout$1(delay);
1047
+ delay *= config.backoffFactor || 2;
1048
+ logger.debug(`Next retry delay: ${delay}ms`);
1049
+ } catch (err) {
1050
+ if (typeof err === "object" && err !== null && "message" in err && err.message === "not_found") {
1051
+ logger.warn(`Document ${id} not found during patch operation`);
1052
+ return {
1053
+ ok: false,
1054
+ statusCode: 404,
1055
+ error: "not_found"
1056
+ };
1057
+ }
1058
+ attempts++;
1059
+ if (attempts > maxRetries) {
1060
+ const error = `Failed to patch after ${maxRetries} attempts: ${err}`;
1061
+ logger.error(error);
1062
+ return {
1063
+ ok: false,
1064
+ statusCode: 500,
1065
+ error
1066
+ };
1067
+ }
1068
+ logger.warn(`Error during patch attempt ${attempts}: ${err}`);
1069
+ await setTimeout$1(delay);
1070
+ logger.debug(`Retrying after ${delay}ms`);
1071
+ }
1072
+ }
1073
+ };
1074
+
1075
+ //#endregion
1076
+ //#region impl/query.mts
1077
+ /**
1078
+ * Executes a CouchDB view query with optional schema validation and automatic handling
1079
+ * of HTTP method selection, query string construction, and retryable errors.
1080
+ *
1081
+ * @remarks
1082
+ * When using the validation feature, each row in the response will be validated against the provided
1083
+ * Types will be inferred from the StandardSchemaV1 supplied in the `options.validate` object.
1084
+ *
1085
+ * @template DocSchema - StandardSchemaV1 used to validate each returned `doc`, if provided.
1086
+ * @template KeySchema - StandardSchemaV1 used to validate each row `key`, if provided.
1087
+ * @template ValueSchema - StandardSchemaV1 used to validate each row `value`, if provided.
1088
+ *
1089
+ * @param _config - CouchDB configuration data that is validated before use.
1090
+ * @param view - Fully qualified design document and view identifier (e.g., `_design/foo/_view/bar`).
1091
+ * @param options - CouchDB view options, including optional validation schemas.
1092
+ *
1093
+ * @returns The parsed view response with rows validated against the supplied schemas.
1094
+ *
1095
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
1096
+ * @throws {Error<Array<StandardSchemaV1.Issue>>} When the configuration or validation schemas fail to parse.
1097
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
1098
+ */
1099
+ async function query(_config, view, options = {}) {
1100
+ const configParseResult = CouchConfig.safeParse(_config);
1101
+ const logger = createLogger(_config);
1102
+ logger.info(`Starting view query: ${view}`);
1103
+ logger.debug("Query options:", ViewOptions.parse(options || {}));
1104
+ if (!configParseResult.success) {
1105
+ logger.error(`Invalid configuration provided: ${z$1.prettifyError(configParseResult.error)}`);
1106
+ throw configParseResult.error;
1107
+ }
1108
+ const config = configParseResult.data;
1109
+ let qs = queryString(options);
1110
+ let method = "get";
1111
+ let payload = null;
1112
+ const mergedOpts = mergeNeedleOpts(config, {
1113
+ json: true,
1114
+ headers: { "Content-Type": "application/json" }
1115
+ });
1116
+ if (typeof options.keys !== "undefined") {
1117
+ const MAX_URL_LENGTH = 2e3;
1118
+ const _options = structuredClone(options);
1119
+ delete _options.keys;
1120
+ qs = queryString(_options);
1121
+ const keysAsString = `keys=${JSON.stringify(options.keys)}`;
1122
+ if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
1123
+ method = "get";
1124
+ if (qs.length > 0) qs += "&";
1125
+ else qs = "";
1126
+ qs += keysAsString;
1127
+ } else {
1128
+ method = "post";
1129
+ payload = { keys: options.keys };
1130
+ }
1131
+ }
1132
+ logger.debug("Generated query string:", qs);
1133
+ const url = `${config.couch}/${view}?${qs}`;
1134
+ let results;
1135
+ try {
1136
+ logger.debug(`Sending ${method} request to: ${url}`);
1137
+ results = method === "get" ? await needle("get", url, mergedOpts) : await needle("post", url, payload, mergedOpts);
1138
+ } catch (err) {
1139
+ logger.error("Network error during query:", err);
1140
+ RetryableError.handleNetworkError(err);
1141
+ }
1142
+ if (!results) {
1143
+ logger.error("No response received from query request");
1144
+ throw new RetryableError("no response", 503);
1145
+ }
1146
+ const body = results.body;
1147
+ if (RetryableError.isRetryableStatusCode(results.statusCode)) {
1148
+ logger.warn(`Retryable status code received: ${results.statusCode}`);
1149
+ throw new RetryableError(body.error || "retryable error during query", results.statusCode);
1150
+ }
1151
+ if (body.error) {
1152
+ logger.error(`Query error: ${JSON.stringify(body)}`);
1153
+ throw new Error(`CouchDB query error: ${body.error} - ${body.reason || ""}`);
1154
+ }
1155
+ if (options.validate && body.rows) body.rows = await parseRows(body.rows, options.validate);
1156
+ logger.info(`Successfully executed view query: ${view}`);
1157
+ logger.debug("Query response:", body);
1158
+ return body;
1159
+ }
1160
+
1161
+ //#endregion
1162
+ //#region impl/utils/trackedEmitter.mts
1163
+ const setupEmitter = (config) => {
1164
+ if (!config["~emitter"]) return { emit: async () => {} };
1165
+ return config["~emitter"];
1166
+ };
1167
+
1168
+ //#endregion
1169
+ //#region impl/utils/transactionErrors.mts
1170
+ var TransactionSetupError = class extends Error {
1171
+ details;
1172
+ constructor(message, details = {}) {
1173
+ super(message);
1174
+ this.name = "TransactionSetupError";
1175
+ this.details = details;
1176
+ }
1177
+ };
1178
+ var TransactionVersionConflictError = class extends Error {
1179
+ conflictingIds;
1180
+ constructor(conflictingIds) {
1181
+ super(`Revision mismatch for documents: ${conflictingIds.join(", ")}`);
1182
+ this.name = "TransactionVersionConflictError";
1183
+ this.conflictingIds = conflictingIds;
1184
+ }
1185
+ };
1186
+ var TransactionBulkOperationError = class extends Error {
1187
+ failedDocs;
1188
+ constructor(failedDocs) {
1189
+ super(`Failed to save documents: ${failedDocs.map((d) => d.id).join(", ")}`);
1190
+ this.name = "TransactionBulkOperationError";
1191
+ this.failedDocs = failedDocs;
1192
+ }
1193
+ };
1194
+ var TransactionRollbackError = class extends Error {
1195
+ originalError;
1196
+ rollbackResults;
1197
+ constructor(message, originalError, rollbackResults) {
1198
+ super(message);
1199
+ this.name = "TransactionRollbackError";
1200
+ this.originalError = originalError;
1201
+ this.rollbackResults = rollbackResults;
1202
+ }
1203
+ };
1204
+
1205
+ //#endregion
1206
+ //#region impl/bulkSave.mts
1207
+ /**
1208
+ * Bulk saves documents to CouchDB using the _bulk_docs endpoint.
1209
+ *
1210
+ * @see
1211
+ * https://docs.couchdb.org/en/stable/api/database/bulk-api.html#db-bulk-docs
1212
+ *
1213
+ * @param {CouchConfigInput} config - The CouchDB configuration.
1214
+ * @param {CouchDocInput[]} docs - An array of documents to save.
1215
+ * @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
1216
+ *
1217
+ * @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
1218
+ * @throws {Error} When CouchDB returns a non-retryable error payload.
1219
+ */
1220
+ const bulkSave = async (config, docs) => {
1221
+ const logger = createLogger(config);
1222
+ if (docs == null || !docs.length) {
1223
+ logger.error("bulkSave called with no docs");
1224
+ throw new Error("no docs provided");
1225
+ }
1226
+ logger.info(`Starting bulk save of ${docs.length} documents`);
1227
+ const url = `${config.couch}/_bulk_docs`;
1228
+ const body = { docs };
1229
+ const mergedOpts = mergeNeedleOpts(config, {
1230
+ json: true,
1231
+ headers: { "Content-Type": "application/json" }
1232
+ });
1233
+ let resp;
1234
+ try {
1235
+ resp = await needle("post", url, body, mergedOpts);
1236
+ } catch (err) {
1237
+ logger.error("Network error during bulk save:", err);
1238
+ RetryableError.handleNetworkError(err);
1239
+ }
1240
+ if (!resp) {
1241
+ logger.error("No response received from bulk save request");
1242
+ throw new RetryableError("no response", 503);
1243
+ }
1244
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
1245
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
1246
+ throw new RetryableError("retryable error during bulk save", resp.statusCode);
1247
+ }
1248
+ if (resp.statusCode !== 201) {
1249
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
1250
+ throw new Error("could not save");
1251
+ }
1252
+ const results = resp?.body || [];
1253
+ return BulkSaveResponse.parse(results);
1254
+ };
1255
+ /**
1256
+ * Performs a bulk save of documents within a transaction context.
1257
+ *
1258
+ * @remarks
1259
+ * This operation ensures that either all documents are saved successfully, or none are, maintaining data consistency.
1260
+ * If any document fails to save, the operation will attempt to roll back all changes.
1261
+ *
1262
+ * The transactionId has to be unique for the lifetime of the app. It is used to prevent two processes from executing the same transaction. It is up to you to craft a transactionId that uniquely represents this transaction, and that also is the same if another process tries to generate it.
1263
+ *
1264
+ * Exceptions to handle:
1265
+ *
1266
+ * `TransactionSetupError` Thrown if the transaction document cannot be created. Usually because it already exists
1267
+ * `TransactionVersionConflictError` Thrown if there are version conflicts with existing documents.
1268
+ * `TransactionBulkOperationError` Thrown if the bulk save operation fails for some documents.
1269
+ * `TransactionRollbackError` Thrown if the rollback operation fails after a transaction failure.
1270
+ *
1271
+ * @example
1272
+ * ```ts
1273
+ * const docsToSave = [
1274
+ * { _id: 'doc1', foo: 'bar' },
1275
+ * { _id: 'doc2', foo: 'baz' }
1276
+ * ];
1277
+ *
1278
+ * try {
1279
+ * const results = await bulkSaveTransaction(config, 'unique-transaction-id', docsToSave);
1280
+ * console.log('Bulk save successful:', results);
1281
+ * } catch (error) {
1282
+ * console.error('Bulk save transaction failed:', error);
1283
+ * }
1284
+ * ```
1285
+ *
1286
+ * @param {CouchConfigInput} config - The CouchDB configuration.
1287
+ * @param {string} transactionId - A unique identifier for the transaction.
1288
+ * @param {CouchDocInput[]} docs - An array of documents to save.
1289
+ * @returns {Promise<BulkSaveResponse>} - The transaction save results.
1290
+ * @throws {TransactionSetupError} When the transaction document cannot be created.
1291
+ * @throws {TransactionVersionConflictError} When there are version conflicts with existing documents.
1292
+ * @throws {TransactionBulkOperationError} When the bulk save operation fails for some documents.
1293
+ * @throws {TransactionRollbackError} When the rollback operation fails after a transaction failure.
1294
+ */
1295
+ const bulkSaveTransaction = async (config, transactionId, docs) => {
1296
+ const emitter = setupEmitter(config);
1297
+ const logger = createLogger(config);
1298
+ const retryOptions = {
1299
+ maxRetries: config.maxRetries ?? 10,
1300
+ initialDelay: config.initialDelay ?? 1e3,
1301
+ backoffFactor: config.backoffFactor ?? 2
1302
+ };
1303
+ const _put = config.bindWithRetry ? withRetry(put.bind(null, config), retryOptions) : put.bind(null, config);
1304
+ logger.info(`Starting bulk save transaction ${transactionId} for ${docs.length} documents`);
1305
+ const transactionDoc = {
1306
+ _id: `txn:${transactionId}`,
1307
+ _rev: null,
1308
+ type: "transaction",
1309
+ status: "pending",
1310
+ changes: docs,
1311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1312
+ };
1313
+ let transactionResponse = await _put(transactionDoc);
1314
+ logger.debug("Transaction document created:", transactionDoc, transactionResponse);
1315
+ await emitter.emit("transaction-created", {
1316
+ transactionResponse,
1317
+ txnDoc: transactionDoc
1318
+ });
1319
+ if (transactionResponse.error) throw new TransactionSetupError("Failed to create transaction document", {
1320
+ error: transactionResponse.error,
1321
+ response: transactionResponse
1322
+ });
1323
+ const existingDocs = await bulkGetDictionary(config, docs.map((d) => d._id));
1324
+ logger.debug("Fetched current revisions of documents:", existingDocs);
1325
+ await emitter.emit("transaction-revs-fetched", existingDocs);
1326
+ /** @type {string[]} */
1327
+ const revErrors = [];
1328
+ docs.forEach((d) => {
1329
+ if (!d._id) return;
1330
+ if (existingDocs.found[d._id] && existingDocs.found[d._id]._rev !== d._rev) revErrors.push(d._id);
1331
+ if (existingDocs.notFound[d._id] && d._rev) revErrors.push(d._id);
1332
+ });
1333
+ if (revErrors.length > 0) throw new TransactionVersionConflictError(revErrors);
1334
+ logger.debug("Checked document revisions:", existingDocs);
1335
+ await emitter.emit("transaction-revs-checked", existingDocs);
1336
+ const providedDocsById = {};
1337
+ docs.forEach((d) => {
1338
+ if (!d._id) return;
1339
+ providedDocsById[d._id] = d;
1340
+ });
1341
+ const newDocsToRollback = [];
1342
+ const potentialExistingDocsToRollback = [];
1343
+ const failedDocs = [];
1344
+ try {
1345
+ logger.info("Transaction started:", transactionDoc);
1346
+ await emitter.emit("transaction-started", transactionDoc);
1347
+ const results = await bulkSave(config, docs);
1348
+ logger.info("Transaction updates applied:", results);
1349
+ await emitter.emit("transaction-updates-applied", results);
1350
+ results.forEach((r) => {
1351
+ if (!r.id) return;
1352
+ if (!r.error) {
1353
+ if (existingDocs.notFound[r.id]) newDocsToRollback.push(r);
1354
+ if (existingDocs.found[r.id]) potentialExistingDocsToRollback.push(r);
1355
+ } else failedDocs.push(r);
1356
+ });
1357
+ if (failedDocs.length > 0) throw new TransactionBulkOperationError(failedDocs);
1358
+ transactionDoc.status = "completed";
1359
+ transactionDoc._rev = transactionResponse.rev;
1360
+ transactionResponse = await _put(transactionDoc);
1361
+ logger.info("Transaction completed:", transactionDoc);
1362
+ await emitter.emit("transaction-completed", {
1363
+ transactionResponse,
1364
+ transactionDoc
1365
+ });
1366
+ if (transactionResponse.statusCode !== 201) logger.error("Failed to update transaction status to completed");
1367
+ return results;
1368
+ } catch (error) {
1369
+ logger.error("Transaction failed, attempting rollback:", error);
1370
+ const toRollback = [];
1371
+ potentialExistingDocsToRollback.forEach((row) => {
1372
+ if (!row.id || !row.rev) return;
1373
+ const doc = existingDocs.found[row.id];
1374
+ doc._rev = row.rev;
1375
+ toRollback.push(doc);
1376
+ });
1377
+ newDocsToRollback.forEach((d) => {
1378
+ if (!d.id || !d.rev) return;
1379
+ const before = JSON.parse(JSON.stringify(providedDocsById[d.id]));
1380
+ before._rev = d.rev;
1381
+ before._deleted = true;
1382
+ toRollback.push(before);
1383
+ });
1384
+ const bulkRollbackResult = await bulkSave(config, toRollback);
1385
+ let status = "rolled_back";
1386
+ bulkRollbackResult.forEach((r) => {
1387
+ if (r.error) status = "rollback_failed";
1388
+ });
1389
+ logger.warn("Transaction rolled back:", {
1390
+ bulkRollbackResult,
1391
+ status
1392
+ });
1393
+ await emitter.emit("transaction-rolled-back", {
1394
+ bulkRollbackResult,
1395
+ status
1396
+ });
1397
+ transactionDoc.status = status;
1398
+ transactionDoc._rev = transactionResponse.rev || null;
1399
+ transactionResponse = await _put(transactionDoc);
1400
+ logger.warn("Transaction rollback status updated:", transactionDoc);
1401
+ await emitter.emit("transaction-rolled-back-status", {
1402
+ transactionResponse,
1403
+ transactionDoc
1404
+ });
1405
+ if (transactionResponse.statusCode !== 201) logger.error("Failed to update transaction status to rolled_back");
1406
+ throw new TransactionRollbackError("Transaction failed and rollback was unsuccessful", error, bulkRollbackResult);
1407
+ }
1408
+ };
1409
+
1410
+ //#endregion
1411
+ //#region impl/remove.mts
1412
+ const remove = async (configInput, id, rev) => {
1413
+ const config = CouchConfig.parse(configInput);
1414
+ const logger = createLogger(config);
1415
+ const url = `${config.couch}/${id}?rev=${rev}`;
1416
+ const mergedOpts = mergeNeedleOpts(config, {
1417
+ json: true,
1418
+ headers: { "Content-Type": "application/json" }
1419
+ });
1420
+ logger.info(`Deleting document with id: ${id}`);
1421
+ let resp;
1422
+ try {
1423
+ resp = await needle("delete", url, null, mergedOpts);
1424
+ } catch (err) {
1425
+ logger.error("Error during delete operation:", err);
1426
+ RetryableError.handleNetworkError(err);
1427
+ }
1428
+ if (!resp) {
1429
+ logger.error("No response received from delete request");
1430
+ throw new RetryableError("no response", 503);
1431
+ }
1432
+ let result;
1433
+ if (typeof resp.body === "string") try {
1434
+ result = JSON.parse(resp.body);
1435
+ } catch {
1436
+ result = {};
1437
+ }
1438
+ else result = resp.body || {};
1439
+ result.statusCode = resp.statusCode;
1440
+ if (resp.statusCode === 404) {
1441
+ logger.warn(`Document not found for deletion: ${id}`);
1442
+ result.ok = false;
1443
+ result.error = "not_found";
1444
+ return CouchPutResponse.parse(result);
1445
+ }
1446
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
1447
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
1448
+ throw new RetryableError(result.reason || "retryable error", resp.statusCode);
1449
+ }
1450
+ if (resp.statusCode !== 200) {
1451
+ logger.error(`Unexpected status code: ${resp.statusCode}`);
1452
+ throw new Error(result.reason || "failed");
1453
+ }
1454
+ logger.info(`Successfully deleted document: ${id}`);
1455
+ return CouchPutResponse.parse(result);
1456
+ };
1457
+
1458
+ //#endregion
1459
+ //#region impl/bulkRemove.mts
1460
+ /**
1461
+ * Removes multiple documents from a CouchDB database using the _bulk_docs endpoint.
1462
+ * It first retrieves the documents by their IDs, marks them as deleted, and then
1463
+ * sends them back to the database for deletion.
1464
+ *
1465
+ * See https://docs.couchdb.org/en/stable/api/database/bulk-api.html#post--db-_bulk_docs
1466
+ *
1467
+ * @param configInput - The CouchDB configuration input.
1468
+ * @param ids - An array of document IDs to be removed.
1469
+ * @returns A promise that resolves to an array of results from the bulk delete operation.
1470
+ *
1471
+ * @example
1472
+ * ```ts
1473
+ * const config: CouchConfigInput = {
1474
+ * couch: 'http://localhost:5984/mydb',
1475
+ * useConsoleLogger: true
1476
+ * };
1477
+ * const idsToRemove = ['doc1', 'doc2', 'doc3'];
1478
+ * const results = await bulkRemove(config, idsToRemove);
1479
+ * console.log(results);
1480
+ * ```
1481
+ *
1482
+ * @throws Will throw an error if the provided configuration is invalid or if the bulk delete operation fails.
1483
+ */
1484
+ const bulkRemove = async (configInput, ids) => {
1485
+ const config = CouchConfig.parse(configInput);
1486
+ const logger = createLogger(config);
1487
+ logger.info(`Starting bulk remove for ${ids.length} documents`);
1488
+ const resp = await bulkGet(config, ids);
1489
+ const toRemove = [];
1490
+ resp.rows?.forEach((row) => {
1491
+ if (!row.doc) return;
1492
+ try {
1493
+ const d = CouchDoc.parse(row.doc);
1494
+ d._deleted = true;
1495
+ toRemove.push(d);
1496
+ } catch (e) {
1497
+ logger.warn(`Invalid document structure in bulk remove: ${row.id}`, e);
1498
+ }
1499
+ });
1500
+ if (!toRemove.length) return [];
1501
+ return await bulkSave(config, toRemove);
1502
+ };
1503
+ /**
1504
+ * Removes multiple documents from a CouchDB database by their IDs using individual delete operations.
1505
+ * It first retrieves the documents to get their revision IDs, then deletes each document one by one.
1506
+ *
1507
+ * See https://docs.couchdb.org/en/stable/api/document/common.html#delete--db-docid
1508
+ *
1509
+ * @param configInput - The CouchDB configuration input.
1510
+ * @param ids - An array of document IDs to be removed.
1511
+ * @returns A promise that resolves to an array of results from the individual delete operations.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * const config: CouchConfigInput = {
1516
+ * couch: 'http://localhost:5984/mydb',
1517
+ * useConsoleLogger: true
1518
+ * };
1519
+ * const idsToRemove = ['doc1', 'doc2', 'doc3'];
1520
+ * const results = await bulkRemoveMap(config, idsToRemove);
1521
+ * console.log(results);
1522
+ * ```
1523
+ *
1524
+ * @throws Will throw an error if the provided configuration is invalid or if any delete operation fails.
1525
+ */
1526
+ const bulkRemoveMap = async (configInput, ids) => {
1527
+ const config = CouchConfig.parse(configInput);
1528
+ const logger = createLogger(config);
1529
+ logger.info(`Starting bulk remove map for ${ids.length} documents`);
1530
+ const { rows } = await bulkGet(config, ids, { includeDocs: false });
1531
+ const results = [];
1532
+ for (const row of rows || []) try {
1533
+ if (!row.value?.rev) throw new Error(`no rev found for doc ${row.id}`);
1534
+ if (!row.id) throw new Error(`no id found for doc ${row}`);
1535
+ const result = await remove(config, row.id, row.value.rev);
1536
+ results.push(result);
1537
+ } catch (e) {
1538
+ logger.warn(`Error removing a doc in bulk remove map: ${row.id}`, e);
1539
+ }
1540
+ return results;
1541
+ };
1542
+
1543
+ //#endregion
1544
+ //#region impl/getDBInfo.mts
1545
+ /**
1546
+ * Fetches and returns CouchDB database information.
1547
+ *
1548
+ * @see {@link https://docs.couchdb.org/en/stable/api/database/common.html#get--db | CouchDB API Documentation}
1549
+ *
1550
+ * @param configInput - The CouchDB configuration input.
1551
+ * @returns A promise that resolves to the CouchDB database information.
1552
+ * @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
1553
+ * @throws {Error} `Error` For other non-retryable errors.
1554
+ *
1555
+ * @example
1556
+ * ```ts
1557
+ * import { getDBInfo } from './impl/getDBInfo.mts';
1558
+ *
1559
+ * const config = { couch: 'http://localhost:5984/my-database' };
1560
+ *
1561
+ * getDBInfo(config)
1562
+ * .then(info => {
1563
+ * console.log('Database Info:', info);
1564
+ * })
1565
+ * .catch(err => {
1566
+ * console.error('Error fetching database info:', err);
1567
+ * });
1568
+ * ```
1569
+ */
1570
+ const getDBInfo = async (configInput) => {
1571
+ const config = CouchConfig.parse(configInput);
1572
+ const logger = createLogger(config);
1573
+ const url = `${config.couch}`;
1574
+ let resp;
1575
+ try {
1576
+ resp = await needle("get", url, mergeNeedleOpts(config, {
1577
+ json: true,
1578
+ headers: { "Content-Type": "application/json" }
1579
+ }));
1580
+ } catch (err) {
1581
+ logger.error("Error during get operation:", err);
1582
+ RetryableError.handleNetworkError(err);
1583
+ }
1584
+ if (!resp) {
1585
+ logger.error("No response received from get request");
1586
+ throw new RetryableError("no response", 503);
1587
+ }
1588
+ const result = resp.body;
1589
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
1590
+ logger.warn(`Retryable status code received: ${resp.statusCode}`);
1591
+ throw new RetryableError(result.reason ?? "retryable error", resp.statusCode);
1592
+ }
1593
+ return CouchDBInfo.parse(result);
1594
+ };
1595
+
1596
+ //#endregion
1597
+ //#region schema/sugar/lock.mts
1598
+ const LockDoc = CouchDoc.extend({
1599
+ type: z$1.literal("lock"),
1600
+ locks: z$1.string().describe("the document ID being locked"),
1601
+ lockedAt: z$1.string().describe("ISO timestamp when lock was created"),
1602
+ lockedBy: z$1.string().describe("username of who created the lock")
1603
+ });
1604
+ const LockOptions = z$1.object({
1605
+ enableLocking: z$1.boolean().prefault(true).describe("whether locking is enabled"),
1606
+ username: z$1.string().describe("username to attribute locks to")
1607
+ });
1608
+
1609
+ //#endregion
1610
+ //#region impl/sugar/lock.mts
1611
+ /**
1612
+ * Create a lock document for the specified document ID.
1613
+ * Returns true if the lock was created, false if locking is disabled or a conflict occurred.
1614
+ *
1615
+ * @param configInput CouchDB configuration
1616
+ * @param docId The document ID to lock
1617
+ * @param lockOptions Locking options
1618
+ *
1619
+ * @return True if the lock was created, false otherwise
1620
+ */
1621
+ async function createLock(configInput, docId, lockOptions) {
1622
+ const config = CouchConfig.parse(configInput);
1623
+ const options = LockOptions.parse(lockOptions);
1624
+ const logger = createLogger(config);
1625
+ if (!options.enableLocking) {
1626
+ logger.debug("Locking disabled, returning true without creating lock");
1627
+ return true;
1628
+ }
1629
+ const lock = {
1630
+ _id: `lock-${docId}`,
1631
+ type: "lock",
1632
+ locks: docId,
1633
+ lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
1634
+ lockedBy: options.username
1635
+ };
1636
+ try {
1637
+ const result = await put(config, lock);
1638
+ logger.info(`Lock created for ${docId} by ${options.username}`);
1639
+ return result.ok === true;
1640
+ } catch (error) {
1641
+ if (isConflictError(error)) logger.warn(`Lock conflict for ${docId} - already locked`);
1642
+ else logger.error(`Error creating lock for ${docId}:`, error);
1643
+ return false;
1644
+ }
1645
+ }
1646
+ /**
1647
+ * Remove the lock document for the specified document ID if owned by the caller.
1648
+ *
1649
+ * @param configInput CouchDB configuration
1650
+ * @param docId The document ID to unlock
1651
+ * @param lockOptions Locking options
1652
+ *
1653
+ * @return Promise that resolves when the unlock operation is complete
1654
+ */
1655
+ async function removeLock(configInput, docId, lockOptions) {
1656
+ const config = CouchConfig.parse(configInput);
1657
+ const options = LockOptions.parse(lockOptions);
1658
+ const logger = createLogger(config);
1659
+ if (!options.enableLocking) {
1660
+ logger.debug("Locking disabled, skipping unlock");
1661
+ return;
1662
+ }
1663
+ if (!docId) {
1664
+ logger.warn("No docId provided for unlock");
1665
+ return;
1666
+ }
1667
+ const existingLock = await get(config, `lock-${docId}`);
1668
+ if (!existingLock) {
1669
+ logger.debug(`No lock found for ${docId}`);
1670
+ return;
1671
+ }
1672
+ if (existingLock.lockedBy !== options.username) {
1673
+ logger.warn(`Cannot remove lock for ${docId} - owned by ${existingLock.lockedBy}`);
1674
+ return;
1675
+ }
1676
+ try {
1677
+ await put(config, {
1678
+ ...existingLock,
1679
+ _deleted: true
1680
+ });
1681
+ logger.info(`Lock removed for ${docId}`);
1682
+ } catch (error) {
1683
+ logger.error(`Error removing lock for ${docId}:`, error);
1684
+ }
1685
+ }
1686
+
1687
+ //#endregion
1688
+ //#region schema/sugar/watch.mts
1689
+ const WatchOptions = z$1.object({
1690
+ include_docs: z$1.boolean().default(false),
1691
+ maxRetries: z$1.number().describe("maximum number of retries before giving up"),
1692
+ initialDelay: z$1.number().describe("initial delay between retries in milliseconds"),
1693
+ maxDelay: z$1.number().describe("maximum delay between retries in milliseconds")
1694
+ }).partial();
1695
+
1696
+ //#endregion
1697
+ //#region impl/sugar/watch.mts
1698
+ /**
1699
+ * Watch for changes to specified document IDs in CouchDB.
1700
+ * Calls the onChange callback for each change detected.
1701
+ * Returns an emitter with methods to listen for events and stop watching.
1702
+ *
1703
+ * @param configInput CouchDB configuration
1704
+ * @param docIds Document ID or array of document IDs to watch
1705
+ * @param onChange Callback function called on each change
1706
+ * @param optionsInput Watch options
1707
+ *
1708
+ * @return WatchEmitter with methods to manage the watch
1709
+ */
1710
+ function watchDocs(configInput, docIds, onChange, optionsInput = {}) {
1711
+ const config = CouchConfig.parse(configInput);
1712
+ const options = WatchOptions.parse(optionsInput);
1713
+ const logger = createLogger(config);
1714
+ const emitter = new EventEmitter();
1715
+ let lastSeq = null;
1716
+ let stopping = false;
1717
+ let retryCount = 0;
1718
+ let currentRequest = null;
1719
+ const maxRetries = options.maxRetries || 10;
1720
+ const initialDelay = options.initialDelay || 1e3;
1721
+ const maxDelay = options.maxDelay || 3e4;
1722
+ const _docIds = Array.isArray(docIds) ? docIds : [docIds];
1723
+ if (_docIds.length === 0) throw new Error("docIds must be a non-empty array");
1724
+ if (_docIds.length > 100) throw new Error("docIds must be an array of 100 or fewer elements");
1725
+ const connect = async () => {
1726
+ if (stopping) return;
1727
+ const feed = "continuous";
1728
+ const includeDocs = options.include_docs ?? false;
1729
+ const ids = _docIds.join("\",\"");
1730
+ const url = `${config.couch}/_changes?feed=${feed}&since=${lastSeq}&include_docs=${includeDocs}&filter=_doc_ids&doc_ids=["${ids}"]`;
1731
+ const mergedOpts = mergeNeedleOpts(config, {
1732
+ json: false,
1733
+ headers: { "Content-Type": "application/json" },
1734
+ parse_response: false
1735
+ });
1736
+ let buffer = "";
1737
+ currentRequest = needle.get(url, mergedOpts);
1738
+ currentRequest.on("data", (chunk) => {
1739
+ buffer += chunk.toString();
1740
+ const lines = buffer.split("\n");
1741
+ buffer = lines.pop() || "";
1742
+ for (const line of lines) if (line.trim()) try {
1743
+ const change = JSON.parse(line);
1744
+ if (!change.id) return null;
1745
+ logger.debug(`Change detected, watching [${_docIds}]`, change);
1746
+ lastSeq = change.seq || change.last_seq;
1747
+ emitter.emit("change", change);
1748
+ } catch (err) {
1749
+ logger.error("Error parsing change:", err, "Line:", line);
1750
+ }
1751
+ });
1752
+ currentRequest.on("response", (response) => {
1753
+ logger.debug(`Received response with status code, watching [${_docIds}]: ${response.statusCode}`);
1754
+ if (RetryableError.isRetryableStatusCode(response.statusCode)) {
1755
+ logger.warn(`Retryable status code received: ${response.statusCode}`);
1756
+ currentRequest?.destroy();
1757
+ handleReconnect();
1758
+ } else retryCount = 0;
1759
+ });
1760
+ currentRequest.on("error", async (err) => {
1761
+ if (stopping) {
1762
+ logger.info("stopping in progress, ignore stream error");
1763
+ return;
1764
+ }
1765
+ logger.error(`Network error during stream, watching [${_docIds}]:`, err.toString());
1766
+ try {
1767
+ RetryableError.handleNetworkError(err);
1768
+ } catch (filteredError) {
1769
+ if (filteredError instanceof RetryableError) {
1770
+ logger.info(`Retryable error, watching [${_docIds}]:`, filteredError.toString());
1771
+ handleReconnect();
1772
+ } else {
1773
+ logger.error(`Non-retryable error, watching [${_docIds}]`, filteredError?.toString());
1774
+ emitter.emit("error", filteredError);
1775
+ }
1776
+ }
1777
+ });
1778
+ currentRequest.on("end", () => {
1779
+ if (buffer.trim()) try {
1780
+ const change = JSON.parse(buffer);
1781
+ logger.debug("Final change detected:", change);
1782
+ emitter.emit("change", change);
1783
+ } catch (err) {
1784
+ logger.error("Error parsing final change:", err);
1785
+ }
1786
+ logger.info("Stream completed. Last seen seq: ", lastSeq);
1787
+ emitter.emit("end", { lastSeq });
1788
+ if (!stopping) handleReconnect();
1789
+ });
1790
+ };
1791
+ const handleReconnect = async () => {
1792
+ if (stopping || retryCount >= maxRetries) {
1793
+ if (retryCount >= maxRetries) {
1794
+ logger.error(`Max retries (${maxRetries}) reached, giving up`);
1795
+ emitter.emit("error", /* @__PURE__ */ new Error("Max retries reached"));
1796
+ }
1797
+ return;
1798
+ }
1799
+ const delay = Math.min(initialDelay * Math.pow(2, retryCount), maxDelay);
1800
+ retryCount++;
1801
+ logger.info(`Attempting to reconnect in ${delay}ms (attempt ${retryCount} of ${maxRetries})`);
1802
+ await setTimeout$1(delay);
1803
+ try {
1804
+ connect();
1805
+ } catch (err) {
1806
+ logger.error("Error during reconnection:", err);
1807
+ handleReconnect();
1808
+ }
1809
+ };
1810
+ connect();
1811
+ emitter.on("change", onChange);
1812
+ return {
1813
+ on: (event, listener) => emitter.on(event, listener),
1814
+ removeListener: (event, listener) => emitter.removeListener(event, listener),
1815
+ stop: () => {
1816
+ stopping = true;
1817
+ if (currentRequest) currentRequest.destroy();
1818
+ emitter.emit("end", { lastSeq });
1819
+ emitter.removeAllListeners();
1820
+ }
1821
+ };
1822
+ }
1823
+
1824
+ //#endregion
1825
+ //#region impl/bindConfig.mts
1826
+ /**
1827
+ * Build a validated binding that exposes CouchDB helpers plus an options() helper for overrides.
1828
+ * @param config The CouchDB configuration
1829
+ * @returns A bound instance with CouchDB operations and an options() method for overrides
1830
+ */
1831
+ const bindConfig = (config) => {
1832
+ const funcs = doBind(CouchConfig.parse(config));
1833
+ const reconfigure = (overrides) => {
1834
+ return bindConfig({
1835
+ ...config,
1836
+ ...overrides
1837
+ });
1838
+ };
1839
+ return {
1840
+ ...funcs,
1841
+ options: reconfigure
1842
+ };
1843
+ };
1844
+ /**
1845
+ * @internal
1846
+ *
1847
+ * Helper to bind a function to a config, optionally wrapping it with retry logic.
1848
+ * Casts to the appropriate bound function type.
1849
+ * @param func The function to bind
1850
+ * @param config The CouchDB configuration
1851
+ * @returns The bound function, possibly wrapped with retry logic
1852
+ */
1853
+ function getBoundWithRetry(func, config) {
1854
+ const bound = func.bind(null, config);
1855
+ if (config.bindWithRetry) return withRetry(bound, {
1856
+ maxRetries: config.maxRetries ?? 10,
1857
+ initialDelay: config.initialDelay ?? 1e3,
1858
+ backoffFactor: config.backoffFactor ?? 2
1859
+ });
1860
+ else return bound;
1861
+ }
1862
+ /**
1863
+ * @internal
1864
+ *
1865
+ * Bind core CouchDB operations to a specific configuration, optionally applying retry wrappers.
1866
+ * @param config The CouchDB configuration
1867
+ * @returns An object with CouchDB operations bound to the provided configuration
1868
+ */
1869
+ function doBind(config) {
1870
+ const retryOptions = {
1871
+ maxRetries: config.maxRetries ?? 10,
1872
+ initialDelay: config.initialDelay ?? 1e3,
1873
+ backoffFactor: config.backoffFactor ?? 2
1874
+ };
1875
+ return {
1876
+ bulkGet: getBoundWithRetry(bulkGet, config),
1877
+ bulkGetDictionary: getBoundWithRetry(bulkGetDictionary, config),
1878
+ get: getBoundWithRetry(get, config),
1879
+ getAtRev: getBoundWithRetry(getAtRev, config),
1880
+ query: getBoundWithRetry(query, config),
1881
+ bulkRemove: config.bindWithRetry ? withRetry(bulkRemove.bind(null, config), retryOptions) : bulkRemove.bind(null, config),
1882
+ bulkRemoveMap: config.bindWithRetry ? withRetry(bulkRemoveMap.bind(null, config), retryOptions) : bulkRemoveMap.bind(null, config),
1883
+ bulkSave: config.bindWithRetry ? withRetry(bulkSave.bind(null, config), retryOptions) : bulkSave.bind(null, config),
1884
+ bulkSaveTransaction: bulkSaveTransaction.bind(null, config),
1885
+ getDBInfo: config.bindWithRetry ? withRetry(getDBInfo.bind(null, config), retryOptions) : getDBInfo.bind(null, config),
1886
+ patch: config.bindWithRetry ? withRetry(patch.bind(null, config), retryOptions) : patch.bind(null, config),
1887
+ patchDangerously: patchDangerously.bind(null, config),
1888
+ put: config.bindWithRetry ? withRetry(put.bind(null, config), retryOptions) : put.bind(null, config),
1889
+ queryStream: config.bindWithRetry ? withRetry(queryStream.bind(null, config), retryOptions) : queryStream.bind(null, config),
1890
+ remove: config.bindWithRetry ? withRetry(remove.bind(null, config), retryOptions) : remove.bind(null, config),
1891
+ createLock: createLock.bind(null, config),
1892
+ removeLock: removeLock.bind(null, config),
1893
+ watchDocs: watchDocs.bind(null, config)
1894
+ };
1895
+ }
1896
+
1897
+ //#endregion
1898
+ export { QueryBuilder, bindConfig, bulkGet, bulkGetDictionary, bulkRemove, bulkRemoveMap, bulkSave, bulkSaveTransaction, createLock, createQuery, get, getAtRev, getDBInfo, patch, patchDangerously, put, query, queryStream, remove, removeLock, watchDocs, withRetry };