hide-a-bed 5.2.7 → 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 (363) 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 +45 -31
  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/log.txt +0 -580
  330. package/schema/bind.d.mts +0 -5461
  331. package/schema/bind.d.mts.map +0 -1
  332. package/schema/bind.mjs +0 -43
  333. package/schema/bulk.d.mts +0 -923
  334. package/schema/bulk.d.mts.map +0 -1
  335. package/schema/bulk.mjs +0 -83
  336. package/schema/changes.d.mts +0 -191
  337. package/schema/changes.d.mts.map +0 -1
  338. package/schema/changes.mjs +0 -59
  339. package/schema/config.d.mts +0 -79
  340. package/schema/config.d.mts.map +0 -1
  341. package/schema/config.mjs +0 -26
  342. package/schema/crud.d.mts +0 -491
  343. package/schema/crud.d.mts.map +0 -1
  344. package/schema/crud.mjs +0 -64
  345. package/schema/patch.d.mts +0 -255
  346. package/schema/patch.d.mts.map +0 -1
  347. package/schema/patch.mjs +0 -42
  348. package/schema/query.d.mts +0 -406
  349. package/schema/query.d.mts.map +0 -1
  350. package/schema/query.mjs +0 -45
  351. package/schema/stream.d.mts +0 -211
  352. package/schema/stream.d.mts.map +0 -1
  353. package/schema/stream.mjs +0 -23
  354. package/schema/sugar/lock.d.mts +0 -238
  355. package/schema/sugar/lock.d.mts.map +0 -1
  356. package/schema/sugar/lock.mjs +0 -50
  357. package/schema/sugar/watch.d.mts +0 -127
  358. package/schema/sugar/watch.d.mts.map +0 -1
  359. package/schema/sugar/watch.mjs +0 -29
  360. package/schema/util.d.mts +0 -160
  361. package/schema/util.d.mts.map +0 -1
  362. package/schema/util.mjs +0 -35
  363. package/types/changes-stream.d.ts +0 -11
@@ -0,0 +1,155 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import { EventEmitter } from 'node:events'
4
+ import needle from 'needle'
5
+ import type { CouchConfigInput } from '../../schema/config.mts'
6
+ import { watchDocs } from './watch.mts'
7
+
8
+ class FakeRequest extends EventEmitter {
9
+ destroyed = false
10
+
11
+ destroy() {
12
+ this.destroyed = true
13
+ this.emit('close')
14
+ }
15
+ }
16
+
17
+ type NeedleRequest = ReturnType<typeof needle.get>
18
+
19
+ const baseConfig = (): CouchConfigInput => ({
20
+ couch: 'http://localhost:5984/watch-test'
21
+ })
22
+
23
+ const waitFor = async (predicate: () => boolean, timeoutMs = 2000, intervalMs = 10) => {
24
+ const startedAt = Date.now()
25
+ while (Date.now() - startedAt <= timeoutMs) {
26
+ if (predicate()) return
27
+ await new Promise(resolve => {
28
+ setTimeout(resolve, intervalMs)
29
+ })
30
+ }
31
+ throw new Error('waitFor timed out')
32
+ }
33
+
34
+ suite('watchDocs', () => {
35
+ test('requires at least one document id', () => {
36
+ assert.throws(() => {
37
+ watchDocs(baseConfig(), [], () => {})
38
+ }, /non-empty array/)
39
+ })
40
+
41
+ test('rejects more than 100 document ids', () => {
42
+ const ids = Array.from({ length: 101 }, (_, index) => `doc-${index}`)
43
+ assert.throws(() => {
44
+ watchDocs(baseConfig(), ids, () => {})
45
+ }, /100 or fewer elements/)
46
+ })
47
+
48
+ test('emits change events for streamed chunks', async t => {
49
+ const requests: FakeRequest[] = []
50
+ const getMock = t.mock.method(needle, 'get', () => {
51
+ const request = new FakeRequest()
52
+ requests.push(request)
53
+ return request as unknown as NeedleRequest
54
+ })
55
+ t.after(() => {
56
+ getMock.mock.restore()
57
+ })
58
+
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const changes: any[] = []
61
+ const watcher = watchDocs(
62
+ baseConfig(),
63
+ ['doc-a', 'doc-b'],
64
+ change => {
65
+ changes.push(change)
66
+ },
67
+ { include_docs: true }
68
+ )
69
+
70
+ await waitFor(() => requests.length === 1)
71
+ const firstRequest = requests[0]
72
+
73
+ firstRequest.emit('data', Buffer.from('{"id":"doc-a","seq":"1"}\n{"id":"doc-b","seq":"2"}\n'))
74
+
75
+ await waitFor(() => changes.length === 2)
76
+
77
+ assert.deepStrictEqual(
78
+ changes.map(change => change.id),
79
+ ['doc-a', 'doc-b']
80
+ )
81
+ const firstArg = getMock.mock.calls[0].arguments[0]
82
+ if (typeof firstArg !== 'string') {
83
+ throw new Error('Expected first argument to be a string')
84
+ }
85
+ assert.match(firstArg, /include_docs=true/)
86
+ assert.match(firstArg, /doc_ids=\["doc-a","doc-b"\]/)
87
+
88
+ watcher.stop()
89
+ })
90
+
91
+ test('reconnects after retryable response status', async t => {
92
+ const requests: FakeRequest[] = []
93
+ const getMock = t.mock.method(needle, 'get', () => {
94
+ const request = new FakeRequest()
95
+ requests.push(request)
96
+ return request as unknown as NeedleRequest
97
+ })
98
+ t.after(() => {
99
+ getMock.mock.restore()
100
+ })
101
+
102
+ const watcher = watchDocs(baseConfig(), 'doc-retry', () => {}, {
103
+ initialDelay: 1,
104
+ maxDelay: 1,
105
+ maxRetries: 3
106
+ })
107
+
108
+ await waitFor(() => requests.length === 1)
109
+ const firstRequest = requests[0]
110
+ firstRequest.emit('response', { statusCode: 503 })
111
+
112
+ await waitFor(() => requests.length === 2)
113
+
114
+ assert.ok(firstRequest.destroyed)
115
+ assert.strictEqual(getMock.mock.callCount(), 2)
116
+
117
+ watcher.stop()
118
+ })
119
+
120
+ test('emits error after exhausting retries', async t => {
121
+ const requests: FakeRequest[] = []
122
+ const getMock = t.mock.method(needle, 'get', () => {
123
+ const request = new FakeRequest()
124
+ requests.push(request)
125
+ return request as unknown as NeedleRequest
126
+ })
127
+ t.after(() => {
128
+ getMock.mock.restore()
129
+ })
130
+
131
+ const errors: Error[] = []
132
+ const watcher = watchDocs(baseConfig(), 'doc-max', () => {}, {
133
+ maxRetries: 2,
134
+ initialDelay: 1,
135
+ maxDelay: 1
136
+ })
137
+ watcher.on('error', err => {
138
+ errors.push(err as unknown as Error)
139
+ })
140
+
141
+ await waitFor(() => requests.length === 1)
142
+ requests[0].emit('error', { code: 'ECONNRESET' })
143
+
144
+ await waitFor(() => requests.length === 2)
145
+ requests[1].emit('error', { code: 'ECONNRESET' })
146
+
147
+ await waitFor(() => requests.length === 3)
148
+ requests[2].emit('error', { code: 'ECONNRESET' })
149
+
150
+ await waitFor(() => errors.length === 1)
151
+ assert.strictEqual(errors[0].message, 'Max retries reached')
152
+
153
+ watcher.stop()
154
+ })
155
+ })
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Represents a network-level error emitted by Node.js or libraries such as `needle`.
3
+ *
4
+ * @public
5
+ */
6
+ export interface NetworkError {
7
+ /**
8
+ * Machine-readable error code describing the network failure.
9
+ */
10
+ code: string
11
+
12
+ /**
13
+ * Optional human-readable message supplied by the underlying library.
14
+ */
15
+ message?: string
16
+ }
17
+
18
+ const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504])
19
+
20
+ const NETWORK_ERROR_STATUS_MAP = {
21
+ ECONNREFUSED: 503,
22
+ ECONNRESET: 503,
23
+ ETIMEDOUT: 503,
24
+ ENETUNREACH: 503,
25
+ ENOTFOUND: 503,
26
+ EPIPE: 503,
27
+ EHOSTUNREACH: 503,
28
+ ESOCKETTIMEDOUT: 503
29
+ } as const satisfies Record<string, number>
30
+
31
+ type NetworkErrorCode = keyof typeof NETWORK_ERROR_STATUS_MAP
32
+
33
+ const isNetworkError = (value: unknown): value is NetworkError & { code: NetworkErrorCode } => {
34
+ if (typeof value !== 'object' || value === null) return false
35
+ const candidate = value as { code?: unknown }
36
+ return typeof candidate.code === 'string' && candidate.code in NETWORK_ERROR_STATUS_MAP
37
+ }
38
+
39
+ /**
40
+ * Error thrown when a requested CouchDB document cannot be found.
41
+ *
42
+ * @remarks
43
+ * The `docId` property exposes the identifier that triggered the failure, which is
44
+ * helpful for logging and retry strategies.
45
+ *
46
+ * @public
47
+ */
48
+ export class NotFoundError extends Error {
49
+ /**
50
+ * Identifier of the missing document.
51
+ */
52
+ readonly docId: string
53
+
54
+ /**
55
+ * Creates a new {@link NotFoundError} instance.
56
+ *
57
+ * @param docId - The identifier of the document that was not found.
58
+ * @param message - Optional custom error message.
59
+ */
60
+ constructor(docId: string, message = 'Document not found') {
61
+ super(message)
62
+ this.name = 'NotFoundError'
63
+ this.docId = docId
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Error signalling that an operation can be retried due to transient conditions.
69
+ *
70
+ * @remarks
71
+ * Use `RetryableError.isRetryableStatusCode` and `RetryableError.handleNetworkError`
72
+ * to detect when a failure should trigger retry logic.
73
+ *
74
+ * @public
75
+ */
76
+ export class RetryableError extends Error {
77
+ /**
78
+ * HTTP status code associated with the retryable failure, when available.
79
+ */
80
+ readonly statusCode?: number
81
+
82
+ /**
83
+ * Creates a new {@link RetryableError} instance.
84
+ *
85
+ * @param message - Detailed description of the failure.
86
+ * @param statusCode - Optional HTTP status code corresponding to the failure.
87
+ */
88
+ constructor(message: string, statusCode?: number) {
89
+ super(message)
90
+ this.name = 'RetryableError'
91
+ this.statusCode = statusCode
92
+ }
93
+
94
+ /**
95
+ * Determines whether the provided status code should be treated as retryable.
96
+ *
97
+ * @param statusCode - HTTP status code returned by CouchDB.
98
+ *
99
+ * @returns `true` if the status code is considered retryable; otherwise `false`.
100
+ */
101
+ static isRetryableStatusCode(statusCode: number | undefined): statusCode is number {
102
+ if (typeof statusCode !== 'number') return false
103
+ return RETRYABLE_STATUS_CODES.has(statusCode)
104
+ }
105
+
106
+ /**
107
+ * Converts low-level network errors into {@link RetryableError} instances when possible.
108
+ *
109
+ * @param err - The error thrown by the underlying HTTP client.
110
+ *
111
+ * @throws {@link RetryableError} When the error maps to a retryable network condition.
112
+ * @throws {*} Re-throws the original error when it cannot be mapped.
113
+ */
114
+ static handleNetworkError(err: unknown): never {
115
+ if (isNetworkError(err)) {
116
+ const statusCode = NETWORK_ERROR_STATUS_MAP[err.code]
117
+ if (statusCode) {
118
+ throw new RetryableError(`Network error: ${err.code}`, statusCode)
119
+ }
120
+ }
121
+
122
+ throw err
123
+ }
124
+ }
125
+
126
+ export function isConflictError(err: unknown): boolean {
127
+ if (typeof err !== 'object' || err === null) return false
128
+ const candidate = err as { statusCode?: unknown }
129
+ return candidate.statusCode === 409
130
+ }
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import { NotFoundError, RetryableError, isConflictError } from './errors.mts'
4
+
5
+ suite('errors', () => {
6
+ test('NotFoundError exposes docId and message', () => {
7
+ const err = new NotFoundError('doc-123')
8
+ assert.strictEqual(err.name, 'NotFoundError')
9
+ assert.strictEqual(err.message, 'Document not found')
10
+ assert.strictEqual(err.docId, 'doc-123')
11
+ })
12
+
13
+ test('NotFoundError accepts custom message', () => {
14
+ const err = new NotFoundError('doc-456', 'missing doc')
15
+ assert.strictEqual(err.message, 'missing doc')
16
+ })
17
+
18
+ test('RetryableError.isRetryableStatusCode identifies retryable statuses', () => {
19
+ const retryable = [408, 429, 500, 502, 503, 504]
20
+ for (const status of retryable) {
21
+ assert.strictEqual(RetryableError.isRetryableStatusCode(status), true)
22
+ }
23
+ assert.strictEqual(RetryableError.isRetryableStatusCode(404), false)
24
+ assert.strictEqual(RetryableError.isRetryableStatusCode(undefined), false)
25
+ })
26
+
27
+ test('handleNetworkError wraps known network failures', () => {
28
+ assert.throws(
29
+ () => RetryableError.handleNetworkError({ code: 'ECONNRESET' }),
30
+ (err: unknown) =>
31
+ err instanceof RetryableError &&
32
+ err.statusCode === 503 &&
33
+ err.message.includes('ECONNRESET')
34
+ )
35
+ })
36
+
37
+ test('handleNetworkError rethrows unknown errors', () => {
38
+ const original = new Error('boom')
39
+ assert.throws(
40
+ () => RetryableError.handleNetworkError(original),
41
+ (err: unknown) => err === original
42
+ )
43
+ })
44
+
45
+ test('handleNetworkError rethrows unrecognized network codes', () => {
46
+ const networkErr = { code: 'UNKNOWN' }
47
+ assert.throws(
48
+ () => RetryableError.handleNetworkError(networkErr),
49
+ (err: unknown) => err === networkErr
50
+ )
51
+ })
52
+
53
+ test('isConflictError detects statusCode 409', () => {
54
+ assert.strictEqual(isConflictError({ statusCode: 409 }), true)
55
+ assert.strictEqual(isConflictError({ statusCode: 412 }), false)
56
+ assert.strictEqual(isConflictError(null), false)
57
+ })
58
+ })
@@ -0,0 +1,62 @@
1
+ import type { CouchConfigInput } from '../../schema/config.mts'
2
+
3
+ type LoggerMethod = (...args: unknown[]) => void
4
+
5
+ export type Logger = {
6
+ error: LoggerMethod
7
+ warn: LoggerMethod
8
+ info: LoggerMethod
9
+ debug: LoggerMethod
10
+ }
11
+
12
+ type FunctionLogger = (level: keyof Logger, ...args: unknown[]) => void
13
+
14
+ const noop: LoggerMethod = () => {}
15
+
16
+ const createConsoleLogger = (): Logger => ({
17
+ error: (...args) => console.error(...args),
18
+ warn: (...args) => console.warn(...args),
19
+ info: (...args) => console.info(...args),
20
+ debug: (...args) => console.debug(...args)
21
+ })
22
+
23
+ const createNoopLogger = (): Logger => ({
24
+ error: noop,
25
+ warn: noop,
26
+ info: noop,
27
+ debug: noop
28
+ })
29
+
30
+ export function createLogger(config: CouchConfigInput): Logger {
31
+ if (config['~normalizedLogger']) {
32
+ return config['~normalizedLogger']
33
+ }
34
+
35
+ if (!config.logger) {
36
+ const normalized = config.useConsoleLogger ? createConsoleLogger() : createNoopLogger()
37
+ config['~normalizedLogger'] = normalized
38
+ return normalized
39
+ }
40
+
41
+ if (typeof config.logger === 'function') {
42
+ const loggerFn = config.logger as FunctionLogger
43
+ const normalized: Logger = {
44
+ error: (...args) => loggerFn('error', ...args),
45
+ warn: (...args) => loggerFn('warn', ...args),
46
+ info: (...args) => loggerFn('info', ...args),
47
+ debug: (...args) => loggerFn('debug', ...args)
48
+ }
49
+ config['~normalizedLogger'] = normalized
50
+ return normalized
51
+ }
52
+
53
+ const loggerObj = config.logger as Partial<Logger>
54
+ const normalized: Logger = {
55
+ error: loggerObj.error ?? noop,
56
+ warn: loggerObj.warn ?? noop,
57
+ info: loggerObj.info ?? noop,
58
+ debug: loggerObj.debug ?? noop
59
+ }
60
+ config['~normalizedLogger'] = normalized
61
+ return normalized
62
+ }
@@ -0,0 +1,129 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import type { CouchConfigInput } from '../../schema/config.mts'
4
+ import { createLogger, type Logger } from './logger.mts'
5
+
6
+ const baseConfig = (): CouchConfigInput => ({
7
+ couch: 'http://localhost:5984'
8
+ })
9
+
10
+ suite('createLogger', () => {
11
+ test('returns cached logger when present', () => {
12
+ const cached: Logger = {
13
+ error: () => {},
14
+ warn: () => {},
15
+ info: () => {},
16
+ debug: () => {}
17
+ }
18
+ const config: CouchConfigInput = {
19
+ ...baseConfig(),
20
+ '~normalizedLogger': cached
21
+ }
22
+
23
+ const logger = createLogger(config)
24
+
25
+ assert.strictEqual(logger, cached)
26
+ })
27
+
28
+ test('uses console logger when requested', () => {
29
+ const errorCalls: unknown[][] = []
30
+ const warnCalls: unknown[][] = []
31
+ const infoCalls: unknown[][] = []
32
+ const debugCalls: unknown[][] = []
33
+
34
+ const originalError = console.error
35
+ const originalWarn = console.warn
36
+ const originalInfo = console.info
37
+ const originalDebug = console.debug
38
+
39
+ console.error = (...args: unknown[]) => {
40
+ errorCalls.push(args)
41
+ }
42
+ console.warn = (...args: unknown[]) => {
43
+ warnCalls.push(args)
44
+ }
45
+ console.info = (...args: unknown[]) => {
46
+ infoCalls.push(args)
47
+ }
48
+ console.debug = (...args: unknown[]) => {
49
+ debugCalls.push(args)
50
+ }
51
+
52
+ try {
53
+ const config: CouchConfigInput = {
54
+ ...baseConfig(),
55
+ useConsoleLogger: true
56
+ }
57
+
58
+ const logger = createLogger(config)
59
+
60
+ logger.error('boom')
61
+ logger.warn('warn', 123)
62
+ logger.info('info')
63
+ logger.debug('debug')
64
+
65
+ assert.strictEqual(config['~normalizedLogger'], logger)
66
+ assert.deepStrictEqual(errorCalls, [['boom']])
67
+ assert.deepStrictEqual(warnCalls, [['warn', 123]])
68
+ assert.deepStrictEqual(infoCalls, [['info']])
69
+ assert.deepStrictEqual(debugCalls, [['debug']])
70
+ } finally {
71
+ console.error = originalError
72
+ console.warn = originalWarn
73
+ console.info = originalInfo
74
+ console.debug = originalDebug
75
+ }
76
+ })
77
+
78
+ test('creates no-op logger when none provided', () => {
79
+ const config = baseConfig()
80
+ const logger = createLogger(config)
81
+
82
+ assert.strictEqual(config['~normalizedLogger'], logger)
83
+ assert.doesNotThrow(() => logger.error('noop'))
84
+ assert.doesNotThrow(() => logger.warn('noop'))
85
+ assert.doesNotThrow(() => logger.info('noop'))
86
+ assert.doesNotThrow(() => logger.debug('noop'))
87
+ })
88
+
89
+ test('wraps function logger', () => {
90
+ const calls: Array<{ level: string; args: unknown[] }> = []
91
+ const fnLogger = (level: string, ...args: unknown[]) => {
92
+ calls.push({ level, args })
93
+ }
94
+
95
+ const config: CouchConfigInput = {
96
+ ...baseConfig(),
97
+ logger: fnLogger
98
+ }
99
+
100
+ const logger = createLogger(config)
101
+ logger.info('hello', 42)
102
+ logger.error('problem')
103
+
104
+ assert.deepStrictEqual(calls, [
105
+ { level: 'info', args: ['hello', 42] },
106
+ { level: 'error', args: ['problem'] }
107
+ ])
108
+ })
109
+
110
+ test('fills missing methods on object logger', () => {
111
+ let warnCount = 0
112
+ const config: CouchConfigInput = {
113
+ ...baseConfig(),
114
+ logger: {
115
+ warn: () => {
116
+ warnCount++
117
+ }
118
+ }
119
+ }
120
+
121
+ const logger = createLogger(config)
122
+ logger.warn('watch out')
123
+
124
+ assert.strictEqual(warnCount, 1)
125
+ assert.doesNotThrow(() => logger.error('ignored'))
126
+ assert.doesNotThrow(() => logger.info('ignored'))
127
+ assert.doesNotThrow(() => logger.debug('ignored'))
128
+ })
129
+ })
@@ -0,0 +1,16 @@
1
+ import { MergeNeedleOpts } from '../../schema/util.mts'
2
+
3
+ export const mergeNeedleOpts = MergeNeedleOpts.implement((config, opts) => {
4
+ if (config.needleOpts) {
5
+ return {
6
+ ...opts,
7
+ ...config.needleOpts,
8
+ headers: {
9
+ ...opts.headers,
10
+ ...(config.needleOpts.headers ?? {})
11
+ }
12
+ }
13
+ }
14
+
15
+ return opts
16
+ })
@@ -0,0 +1,117 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { ViewRow } from '../../schema/couch/couch.output.schema.ts'
3
+ import type { StandardSchemaV1 } from '../../types/standard-schema.ts'
4
+ import { z } from 'zod'
5
+
6
+ export type OnInvalidDocAction = 'throw' | 'skip'
7
+
8
+ export async function parseRows<
9
+ DocSchema extends StandardSchemaV1 = StandardSchemaV1<any>,
10
+ KeySchema extends StandardSchemaV1 = StandardSchemaV1<any>,
11
+ ValueSchema extends StandardSchemaV1 = StandardSchemaV1<any>
12
+ >(
13
+ rows: unknown,
14
+ options: {
15
+ onInvalidDoc?: OnInvalidDocAction
16
+ docSchema?: DocSchema
17
+ keySchema?: KeySchema
18
+ valueSchema?: ValueSchema
19
+ }
20
+ ) {
21
+ if (!Array.isArray(rows)) {
22
+ throw new Error('invalid rows format')
23
+ }
24
+
25
+ type ParsedRow = {
26
+ id?: string
27
+ key?: StandardSchemaV1.InferOutput<KeySchema>
28
+ value?: StandardSchemaV1.InferOutput<ValueSchema>
29
+ doc?: StandardSchemaV1.InferOutput<DocSchema>
30
+ error?: string
31
+ }
32
+ type RowResult = ParsedRow | 'skip'
33
+ const isFinalRow = (row: RowResult): row is ParsedRow => row !== 'skip'
34
+
35
+ const parsedRows: Array<RowResult> = await Promise.all(
36
+ rows.map(async (row: any) => {
37
+ try {
38
+ /**
39
+ * If no doc is present, parse without doc validation.
40
+ * This allows handling of not-found documents or rows without docs.
41
+ */
42
+ if (row.doc == null) {
43
+ const parsedRow = z.looseObject(ViewRow.shape).parse(row)
44
+ if (options.keySchema) {
45
+ const parsedKey = await options.keySchema['~standard'].validate(row.key)
46
+ if (parsedKey.issues) {
47
+ throw parsedKey.issues
48
+ }
49
+ parsedRow.key = parsedKey.value
50
+ }
51
+ if (options.valueSchema) {
52
+ const parsedValue = await options.valueSchema['~standard'].validate(row.value)
53
+ if (parsedValue.issues) {
54
+ throw parsedValue.issues
55
+ }
56
+ parsedRow.value = parsedValue.value
57
+ }
58
+ return parsedRow
59
+ }
60
+
61
+ let parsedDoc = row.doc
62
+ let parsedKey = row.key
63
+ let parsedValue = row.value
64
+
65
+ if (options.docSchema) {
66
+ const parsedDocRes = await options.docSchema['~standard'].validate(row.doc)
67
+ if (parsedDocRes.issues) {
68
+ if (options.onInvalidDoc === 'skip') {
69
+ // skip invalid doc
70
+ return 'skip'
71
+ } else {
72
+ // throw by default
73
+ throw parsedDocRes.issues
74
+ }
75
+ } else {
76
+ parsedDoc = parsedDocRes.value
77
+ }
78
+ }
79
+
80
+ if (options.keySchema) {
81
+ const parsedKeyRes = await options.keySchema['~standard'].validate(row.key)
82
+ if (parsedKeyRes.issues) {
83
+ throw parsedKeyRes.issues
84
+ } else {
85
+ parsedKey = parsedKeyRes.value
86
+ }
87
+ }
88
+
89
+ if (options.valueSchema) {
90
+ const parsedValueRes = await options.valueSchema['~standard'].validate(row.value)
91
+ if (parsedValueRes.issues) {
92
+ throw parsedValueRes.issues
93
+ } else {
94
+ parsedValue = parsedValueRes.value
95
+ }
96
+ }
97
+
98
+ return {
99
+ ...row,
100
+ doc: parsedDoc,
101
+ key: parsedKey,
102
+ value: parsedValue
103
+ }
104
+ } catch (e) {
105
+ if (options.onInvalidDoc === 'skip') {
106
+ // skip invalid doc
107
+ return 'skip'
108
+ } else {
109
+ // throw by default
110
+ throw e
111
+ }
112
+ }
113
+ })
114
+ )
115
+
116
+ return parsedRows.filter(isFinalRow)
117
+ }