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,319 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import needle from 'needle'
4
+ import type { CouchConfigInput } from '../schema/config.mts'
5
+ import { bulkSave, bulkSaveTransaction } from './bulkSave.mts'
6
+ import { RetryableError } from './utils/errors.mts'
7
+ import {
8
+ TransactionRollbackError,
9
+ TransactionVersionConflictError
10
+ } from './utils/transactionErrors.mts'
11
+ import { TEST_DB_URL } from '../test/setup-db.mts'
12
+
13
+ const baseConfig: CouchConfigInput = {
14
+ couch: TEST_DB_URL,
15
+ useConsoleLogger: true
16
+ }
17
+
18
+ const transactionBaseConfig: CouchConfigInput = {
19
+ couch: TEST_DB_URL,
20
+ bindWithRetry: false,
21
+ useConsoleLogger: true
22
+ }
23
+
24
+ type EventRecord = { event: string; payload: unknown }
25
+
26
+ function createTestEmitter() {
27
+ const events: EventRecord[] = []
28
+ const handlers = new Map<string, Array<(payload: unknown) => Promise<void> | void>>()
29
+
30
+ return {
31
+ events,
32
+ on(event: string, handler: (payload: unknown) => Promise<void> | void) {
33
+ const list = handlers.get(event) ?? []
34
+ list.push(handler)
35
+ handlers.set(event, list)
36
+ },
37
+ async emit(event: string, payload: unknown) {
38
+ events.push({ event, payload })
39
+ const list = handlers.get(event)
40
+ if (!list) return
41
+ for (const handler of list) {
42
+ await handler(payload)
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ async function saveDoc(dbUrl: string, id: string, body: Record<string, unknown>) {
49
+ const response = await needle('put', `${dbUrl}/${id}`, { _id: id, ...body }, { json: true })
50
+
51
+ if (response.statusCode !== 201 && response.statusCode !== 200) {
52
+ throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
53
+ }
54
+
55
+ return response.body as { rev: string }
56
+ }
57
+
58
+ async function getDocFrom(dbUrl: string, id: string) {
59
+ return needle('get', `${dbUrl}/${id}`, null, { json: true })
60
+ }
61
+
62
+ async function getDoc(id: string) {
63
+ return getDocFrom(TEST_DB_URL, id)
64
+ }
65
+
66
+ suite('bulkSave', () => {
67
+ test('rejects invalid config arguments', async () => {
68
+ await assert.rejects(async () => {
69
+ // @ts-expect-error intentionally passing unsupported option
70
+ await bulkSave({ couch: TEST_DB_URL, unsupported: true }, [
71
+ { _id: 'bad-config-doc', count: 1 }
72
+ ])
73
+ })
74
+ })
75
+
76
+ test('throws error if called with no docs', async () => {
77
+ await assert.rejects(async () => {
78
+ // @ts-expect-error testing no docs
79
+ await bulkSave(baseConfig, null)
80
+ })
81
+ await assert.rejects(async () => {
82
+ await bulkSave(baseConfig, [])
83
+ })
84
+ })
85
+
86
+ test('propagates retryable network failures', async () => {
87
+ const offlineConfig: CouchConfigInput = {
88
+ couch: 'http://localhost:6554/offline-bulk-save'
89
+ }
90
+
91
+ await assert.rejects(
92
+ () => bulkSave(offlineConfig, [{ _id: 'offline-doc', count: 1 }]),
93
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
94
+ )
95
+ })
96
+
97
+ test('integration with pouchdb-server', async t => {
98
+ let docTwoInitialRev: string | undefined
99
+ const docs = [
100
+ { _id: `bulk-save-doc-1-${Date.now()}`, type: 'integration', count: 1 },
101
+ { _id: `bulk-save-doc-2-${Date.now()}`, type: 'integration', count: 2 }
102
+ ]
103
+
104
+ await t.test('creates documents via _bulk_docs', async () => {
105
+ const results = await bulkSave(baseConfig, docs)
106
+ assert.strictEqual(results.length, 2)
107
+ const [first, second] = results
108
+ assert.ok(first)
109
+ assert.strictEqual(first.id, docs[0]._id)
110
+ assert.strictEqual(first.ok, true)
111
+ assert.ok(second)
112
+ assert.strictEqual(second.id, docs[1]._id)
113
+ assert.strictEqual(second.ok, true)
114
+
115
+ docTwoInitialRev = second.rev ?? undefined
116
+ assert.ok(typeof docTwoInitialRev === 'string')
117
+
118
+ const { statusCode, body } = await getDoc(docs[0]._id)
119
+ assert.strictEqual(statusCode, 200)
120
+ assert.strictEqual(body?.type, 'integration')
121
+ assert.strictEqual(body?.count, 1)
122
+ })
123
+
124
+ await t.test('updates documents when revision supplied', async () => {
125
+ const current = await getDoc(docs[1]._id)
126
+ assert.strictEqual(current.statusCode, 200)
127
+ const updateResults = await bulkSave(baseConfig, [
128
+ {
129
+ _id: docs[1]._id,
130
+ _rev: current.body?._rev,
131
+ type: 'integration',
132
+ count: 3
133
+ }
134
+ ])
135
+
136
+ assert.strictEqual(updateResults.length, 1)
137
+ const [updated] = updateResults
138
+ assert.ok(updated)
139
+ assert.strictEqual(updated.ok, true)
140
+ assert.ok(updated.rev)
141
+
142
+ const { body } = await getDoc(docs[1]._id)
143
+ assert.strictEqual(body?.count, 3)
144
+ })
145
+
146
+ await t.test('reports conflicts when revision is stale', async () => {
147
+ if (!docTwoInitialRev) throw new Error('Expected initial revision to be captured')
148
+
149
+ const conflictResults = await bulkSave(baseConfig, [
150
+ {
151
+ _id: docs[1]._id,
152
+ _rev: docTwoInitialRev,
153
+ type: 'integration',
154
+ count: 99
155
+ }
156
+ ])
157
+
158
+ assert.strictEqual(conflictResults.length, 1)
159
+ const [conflict] = conflictResults
160
+ assert.ok(conflict)
161
+ assert.strictEqual(conflict.id, docs[1]._id)
162
+ assert.strictEqual(conflict.error, 'conflict')
163
+ assert.ok(conflict.reason)
164
+ })
165
+ })
166
+ })
167
+
168
+ suite('bulkSaveTransaction', () => {
169
+ test('integration with pouchdb-server', async t => {
170
+ await t.test('completes transaction for new and existing docs', async () => {
171
+ const emitter = createTestEmitter()
172
+ const config: CouchConfigInput = {
173
+ ...transactionBaseConfig,
174
+ '~emitter': emitter
175
+ }
176
+
177
+ const existingId = `txn-existing-success-${Date.now()}`
178
+ const newId = `txn-new-success-${Date.now()}`
179
+ const transactionId = `bulk-transaction-success-${Date.now()}`
180
+
181
+ const existing = await saveDoc(TEST_DB_URL, existingId, {
182
+ type: 'transaction',
183
+ count: 1
184
+ })
185
+
186
+ const docs = [
187
+ {
188
+ _id: existingId,
189
+ _rev: existing.rev,
190
+ type: 'transaction',
191
+ count: 2
192
+ },
193
+ { _id: newId, type: 'transaction', count: 1 }
194
+ ]
195
+
196
+ const results = await bulkSaveTransaction(config, transactionId, docs)
197
+ assert.strictEqual(results.length, 2)
198
+ assert.ok(results[0]?.ok)
199
+ assert.strictEqual(results[0]?.id, existingId)
200
+ assert.ok(results[1]?.ok)
201
+ assert.strictEqual(results[1]?.id, newId)
202
+
203
+ const updatedExisting = await getDocFrom(TEST_DB_URL, existingId)
204
+ assert.strictEqual(updatedExisting.statusCode, 200)
205
+ assert.strictEqual(updatedExisting.body?.count, 2)
206
+
207
+ const createdDoc = await getDocFrom(TEST_DB_URL, newId)
208
+ assert.strictEqual(createdDoc.statusCode, 200)
209
+ assert.strictEqual(createdDoc.body?.count, 1)
210
+
211
+ const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
212
+ assert.strictEqual(transactionDoc.statusCode, 200)
213
+ assert.strictEqual(transactionDoc.body?.status, 'completed')
214
+
215
+ assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
216
+ assert.ok(emitter.events.some(({ event }) => event === 'transaction-completed'))
217
+ })
218
+
219
+ await t.test('throws TransactionVersionConflictError when revisions mismatch', async () => {
220
+ const emitter = createTestEmitter()
221
+ const config: CouchConfigInput = {
222
+ ...transactionBaseConfig,
223
+ '~emitter': emitter
224
+ }
225
+
226
+ const docId = `txn-conflict-doc-${Date.now()}`
227
+ const transactionId = `bulk-transaction-conflict-${Date.now()}`
228
+
229
+ const first = await saveDoc(TEST_DB_URL, docId, {
230
+ type: 'conflict',
231
+ count: 1
232
+ })
233
+ await saveDoc(TEST_DB_URL, docId, {
234
+ _rev: first.rev,
235
+ type: 'conflict',
236
+ count: 2
237
+ })
238
+ await assert.rejects(
239
+ () =>
240
+ bulkSaveTransaction(config, transactionId, [
241
+ {
242
+ _id: docId,
243
+ _rev: first.rev,
244
+ type: 'conflict',
245
+ count: 3
246
+ }
247
+ ]),
248
+ (err: unknown) =>
249
+ err instanceof TransactionVersionConflictError && err.conflictingIds.includes(docId)
250
+ )
251
+
252
+ assert.ok(emitter.events.some(({ event }) => event === 'transaction-created'))
253
+ assert.ok(emitter.events.some(({ event }) => event === 'transaction-revs-fetched'))
254
+ })
255
+
256
+ await t.test('rolls back changes when bulk save fails', async () => {
257
+ const emitter = createTestEmitter()
258
+ const config: CouchConfigInput = {
259
+ ...transactionBaseConfig,
260
+ '~emitter': emitter
261
+ }
262
+
263
+ const successId = `txn-rollback-existing-${Date.now()}`
264
+ const conflictId = `txn-rollback-conflict-${Date.now()}`
265
+ const transactionId = `bulk-transaction-rollback-${Date.now()}`
266
+
267
+ const existing = await saveDoc(TEST_DB_URL, successId, {
268
+ type: 'rollback',
269
+ count: 1
270
+ })
271
+ const conflicting = await saveDoc(TEST_DB_URL, conflictId, {
272
+ type: 'rollback',
273
+ count: 1
274
+ })
275
+
276
+ emitter.on('transaction-revs-checked', async () => {
277
+ await needle(
278
+ 'put',
279
+ `${TEST_DB_URL}/${conflictId}`,
280
+ {
281
+ _id: conflictId,
282
+ _rev: conflicting.rev,
283
+ type: 'rollback',
284
+ count: 99
285
+ },
286
+ { json: true }
287
+ )
288
+ })
289
+
290
+ await assert.rejects(
291
+ () =>
292
+ bulkSaveTransaction(config, transactionId, [
293
+ { _id: successId, _rev: existing.rev, type: 'rollback', count: 2 },
294
+ {
295
+ _id: conflictId,
296
+ _rev: conflicting.rev,
297
+ type: 'rollback',
298
+ count: 2
299
+ }
300
+ ]),
301
+ (err: unknown) => err instanceof TransactionRollbackError
302
+ )
303
+
304
+ const rolledBack = await getDocFrom(TEST_DB_URL, successId)
305
+ assert.strictEqual(rolledBack.statusCode, 200)
306
+ assert.strictEqual(rolledBack.body?.count, 1)
307
+
308
+ const conflicted = await getDocFrom(TEST_DB_URL, conflictId)
309
+ assert.strictEqual(conflicted.statusCode, 200)
310
+ assert.strictEqual(conflicted.body?.count, 99)
311
+
312
+ const transactionDoc = await getDocFrom(TEST_DB_URL, `txn:${transactionId}`)
313
+ assert.strictEqual(transactionDoc.statusCode, 200)
314
+ assert.strictEqual(transactionDoc.body?.status, 'rolled_back')
315
+
316
+ assert.ok(emitter.events.some(({ event }) => event === 'transaction-rolled-back'))
317
+ })
318
+ })
319
+ })
package/impl/get.mts ADDED
@@ -0,0 +1,137 @@
1
+ import needle from 'needle'
2
+ import { z } from 'zod'
3
+ import type { CouchConfigInput } from '../schema/config.mts'
4
+ import { createLogger } from './utils/logger.mts'
5
+ import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
6
+ import { RetryableError, NotFoundError } from './utils/errors.mts'
7
+ import type { StandardSchemaV1 } from '../types/standard-schema.ts'
8
+ import { CouchDoc } from '../schema/couch/couch.output.schema.ts'
9
+
10
+ export type GetOptions<DocSchema extends StandardSchemaV1> = {
11
+ validate?: {
12
+ docSchema?: DocSchema
13
+ }
14
+ }
15
+
16
+ type InternalGetOptions<DocSchema extends StandardSchemaV1> = GetOptions<DocSchema> & {
17
+ rev?: string
18
+ }
19
+
20
+ const ValidSchema = z.custom(
21
+ value => {
22
+ return value !== null && typeof value === 'object' && '~standard' in value
23
+ },
24
+ {
25
+ message: 'docSchema must be a valid StandardSchemaV1 schema'
26
+ }
27
+ )
28
+
29
+ export const CouchGetOptions = z.object({
30
+ rev: z.string().optional().describe('the couch doc revision'),
31
+ validate: z
32
+ .object({
33
+ docSchema: ValidSchema.optional()
34
+ })
35
+ .optional()
36
+ .describe('optional document validation rules')
37
+ })
38
+
39
+ async function _getWithOptions<DocSchema extends StandardSchemaV1>(
40
+ config: CouchConfigInput,
41
+ id: string,
42
+ options: InternalGetOptions<DocSchema>
43
+ ): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
44
+ const parsedOptions = CouchGetOptions.parse({
45
+ rev: options.rev,
46
+ validate: options.validate
47
+ })
48
+
49
+ const logger = createLogger(config)
50
+ const rev = parsedOptions.rev
51
+ const path = rev ? `${id}?rev=${rev}` : id
52
+ const url = `${config.couch}/${path}`
53
+
54
+ const httpOptions = {
55
+ json: true,
56
+ headers: {
57
+ 'Content-Type': 'application/json'
58
+ }
59
+ }
60
+
61
+ const requestOptions = mergeNeedleOpts(config, httpOptions)
62
+ logger.info(`Getting document with id: ${id}, rev ${rev ?? 'latest'}`)
63
+
64
+ try {
65
+ const resp = await needle('get', url, null, requestOptions)
66
+ if (!resp) {
67
+ logger.error('No response received from get request')
68
+ throw new RetryableError('no response', 503)
69
+ }
70
+
71
+ const body = resp.body ?? null
72
+
73
+ if (resp.statusCode === 404) {
74
+ if (config.throwOnGetNotFound) {
75
+ const reason = typeof body?.reason === 'string' ? body.reason : 'not_found'
76
+ logger.warn(`Document not found (throwing error): ${id}, rev ${rev ?? 'latest'}`)
77
+ throw new NotFoundError(id, reason)
78
+ }
79
+
80
+ logger.debug(`Document not found (returning undefined): ${id}, rev ${rev ?? 'latest'}`)
81
+ return null
82
+ }
83
+
84
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
85
+ const reason = typeof body?.reason === 'string' ? body.reason : 'retryable error'
86
+ logger.warn(`Retryable status code received: ${resp.statusCode}`)
87
+ throw new RetryableError(reason, resp.statusCode)
88
+ }
89
+
90
+ if (resp.statusCode !== 200) {
91
+ const reason = typeof body?.reason === 'string' ? body.reason : 'failed'
92
+ logger.error(`Unexpected status code: ${resp.statusCode}`)
93
+ throw new Error(reason)
94
+ }
95
+
96
+ const docSchema = (parsedOptions.validate?.docSchema ?? CouchDoc) as DocSchema
97
+ const typedDoc = await docSchema['~standard'].validate(body)
98
+
99
+ if (typedDoc.issues) {
100
+ throw typedDoc.issues
101
+ }
102
+
103
+ logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? 'latest'}`)
104
+ return typedDoc.value
105
+ } catch (err) {
106
+ logger.error('Error during get operation:', err)
107
+ RetryableError.handleNetworkError(err)
108
+ }
109
+ }
110
+
111
+ export async function get<DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
112
+ config: CouchConfigInput,
113
+ id: string,
114
+ options?: GetOptions<DocSchema>
115
+ ): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
116
+ return _getWithOptions<DocSchema>(config, id, options ?? {})
117
+ }
118
+
119
+ export type GetBound = <DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
120
+ id: string,
121
+ options?: GetOptions<DocSchema>
122
+ ) => Promise<StandardSchemaV1.InferOutput<DocSchema> | null>
123
+
124
+ export async function getAtRev<DocSchema extends StandardSchemaV1>(
125
+ config: CouchConfigInput,
126
+ id: string,
127
+ rev: string,
128
+ options?: GetOptions<DocSchema>
129
+ ): Promise<StandardSchemaV1.InferOutput<DocSchema> | null> {
130
+ return _getWithOptions<DocSchema>(config, id, { ...options, rev })
131
+ }
132
+
133
+ export type GetAtRevBound = <DocSchema extends StandardSchemaV1 = typeof CouchDoc>(
134
+ id: string,
135
+ rev: string,
136
+ options?: GetOptions<DocSchema> | undefined
137
+ ) => Promise<StandardSchemaV1.InferOutput<DocSchema> | null>
@@ -0,0 +1,114 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import needle from 'needle'
4
+ import { z } from 'zod'
5
+ import type { CouchConfigInput } from '../schema/config.mts'
6
+ import { get, getAtRev } from './get.mts'
7
+ import { NotFoundError, RetryableError } from './utils/errors.mts'
8
+ import { TEST_DB_URL } from '../test/setup-db.mts'
9
+
10
+ const baseConfig: CouchConfigInput = {
11
+ couch: TEST_DB_URL
12
+ }
13
+
14
+ type DocBody = Record<string, unknown>
15
+
16
+ async function saveDoc(id: string, body: DocBody) {
17
+ const response = await needle(
18
+ 'put',
19
+ `${TEST_DB_URL}/${id}`,
20
+ {
21
+ _id: id,
22
+ ...body
23
+ },
24
+ { json: true }
25
+ )
26
+
27
+ if (response.statusCode !== 201 && response.statusCode !== 200) {
28
+ throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
29
+ }
30
+
31
+ return response.body as { rev: string }
32
+ }
33
+
34
+ suite('get', () => {
35
+ test('integration with pouchdb-server', async t => {
36
+ const doc_valid_id = `doc-valid-${Date.now()}`
37
+ const doc_invalid_id = `doc-invalid-${Date.now()}`
38
+ const doc_rev_id = `doc-rev-${Date.now()}`
39
+ await saveDoc(doc_valid_id, { kind: 'example', count: 7 })
40
+ await saveDoc(doc_invalid_id, { kind: 'example', count: 'oops' })
41
+ const firstRev = await saveDoc(doc_rev_id, { version: 1 })
42
+ await saveDoc(doc_rev_id, { _rev: firstRev.rev, version: 2 })
43
+
44
+ await t.test('returns documents and validates schema', async () => {
45
+ const schema = z.looseObject({
46
+ _id: z.string(),
47
+ kind: z.literal('example'),
48
+ count: z.number()
49
+ })
50
+
51
+ const doc = await get(baseConfig, doc_valid_id, {
52
+ validate: { docSchema: schema }
53
+ })
54
+ assert.ok(doc)
55
+ assert.strictEqual(doc?.kind, 'example')
56
+ assert.strictEqual(doc?.count, 7)
57
+
58
+ await assert.rejects(
59
+ () => get(baseConfig, doc_invalid_id, { validate: { docSchema: schema } }),
60
+ (err: unknown) => {
61
+ return (
62
+ Array.isArray(err) &&
63
+ err[0].message === 'Invalid input: expected number, received string'
64
+ )
65
+ }
66
+ )
67
+ })
68
+
69
+ await t.test('returns null when not found by default', async () => {
70
+ const missing = await get(baseConfig, 'doc-missing')
71
+ assert.strictEqual(missing, null)
72
+ })
73
+
74
+ await t.test('throws NotFoundError when configured', async () => {
75
+ const strictConfig: CouchConfigInput = {
76
+ ...baseConfig,
77
+ throwOnGetNotFound: true
78
+ }
79
+
80
+ await assert.rejects(
81
+ () => get(strictConfig, 'doc-missing'),
82
+ (err: unknown) => err instanceof NotFoundError && err.docId === 'doc-missing'
83
+ )
84
+ })
85
+
86
+ await t.test('getAtRev returns specific revision', async () => {
87
+ const versionedSchema = z.looseObject({
88
+ _id: z.string(),
89
+ version: z.number()
90
+ })
91
+
92
+ const latest = await get(baseConfig, doc_rev_id, {
93
+ validate: { docSchema: versionedSchema }
94
+ })
95
+ assert.strictEqual(latest?.version, 2)
96
+
97
+ const early = await getAtRev(baseConfig, doc_rev_id, firstRev.rev, {
98
+ validate: { docSchema: versionedSchema }
99
+ })
100
+ assert.strictEqual(early?.version, 1)
101
+ })
102
+
103
+ await t.test('propagates retryable network errors', async () => {
104
+ const offlineConfig: CouchConfigInput = {
105
+ couch: 'http://localhost:6553/offline-db'
106
+ }
107
+
108
+ await assert.rejects(
109
+ () => get(offlineConfig, 'doc-valid'),
110
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
111
+ )
112
+ })
113
+ })
114
+ })
@@ -0,0 +1,67 @@
1
+ import needle, { type NeedleResponse } from 'needle'
2
+ import { RetryableError } from './utils/errors.mts'
3
+ import { createLogger } from './utils/logger.mts'
4
+ import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
5
+ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
6
+ import { CouchDBInfo } from '../schema/couch/couch.output.schema.ts'
7
+
8
+ /**
9
+ * Fetches and returns CouchDB database information.
10
+ *
11
+ * @see {@link https://docs.couchdb.org/en/stable/api/database/common.html#get--db | CouchDB API Documentation}
12
+ *
13
+ * @param configInput - The CouchDB configuration input.
14
+ * @returns A promise that resolves to the CouchDB database information.
15
+ * @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
16
+ * @throws {Error} `Error` For other non-retryable errors.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { getDBInfo } from './impl/getDBInfo.mts';
21
+ *
22
+ * const config = { couch: 'http://localhost:5984/my-database' };
23
+ *
24
+ * getDBInfo(config)
25
+ * .then(info => {
26
+ * console.log('Database Info:', info);
27
+ * })
28
+ * .catch(err => {
29
+ * console.error('Error fetching database info:', err);
30
+ * });
31
+ * ```
32
+ */
33
+ export const getDBInfo = async (configInput: CouchConfigInput) => {
34
+ const config = CouchConfig.parse(configInput)
35
+ const logger = createLogger(config)
36
+ const url = `${config.couch}`
37
+
38
+ let resp: NeedleResponse | undefined
39
+ try {
40
+ resp = await needle(
41
+ 'get',
42
+ url,
43
+ mergeNeedleOpts(config, {
44
+ json: true,
45
+ headers: {
46
+ 'Content-Type': 'application/json'
47
+ }
48
+ })
49
+ )
50
+ } catch (err) {
51
+ logger.error('Error during get operation:', err)
52
+ RetryableError.handleNetworkError(err)
53
+ }
54
+
55
+ if (!resp) {
56
+ logger.error('No response received from get request')
57
+ throw new RetryableError('no response', 503)
58
+ }
59
+
60
+ const result = resp.body
61
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
62
+ logger.warn(`Retryable status code received: ${resp.statusCode}`)
63
+ throw new RetryableError(result.reason ?? 'retryable error', resp.statusCode)
64
+ }
65
+
66
+ return CouchDBInfo.parse(result)
67
+ }
@@ -0,0 +1,62 @@
1
+ import assert from 'node:assert/strict'
2
+ import { createServer } from 'node:http'
3
+ import test, { suite } from 'node:test'
4
+ import type { CouchConfigInput } from '../schema/config.mts'
5
+ import { getDBInfo } from './getDBInfo.mts'
6
+ import { RetryableError } from './utils/errors.mts'
7
+ import { TEST_DB_URL } from '../test/setup-db.mts'
8
+
9
+ suite('getDBInfo', () => {
10
+ test('it should throw if provided config is invalid', async () => {
11
+ await assert.rejects(async () => {
12
+ await getDBInfo({
13
+ // @ts-expect-error testing invalid config
14
+ notAnOption: true,
15
+ // @ts-expect-error testing invalid config
16
+ couch: DB_URL,
17
+ useConsoleLogger: true
18
+ })
19
+ })
20
+ })
21
+ test('integration with pouchdb-server', async t => {
22
+ await t.test('returns database metadata', async () => {
23
+ const config: CouchConfigInput = { couch: TEST_DB_URL }
24
+ const info = await getDBInfo(config)
25
+ assert.strictEqual(info.db_name, 'hide-a-bed-test-db')
26
+ assert.ok(typeof info.doc_count === 'number')
27
+ })
28
+ })
29
+
30
+ test('throws RetryableError when server marks response retryable', async t => {
31
+ const port = 8993
32
+ const server = createServer((_req, res) => {
33
+ res.statusCode = 503
34
+ res.setHeader('Content-Type', 'application/json')
35
+ res.end(JSON.stringify({ reason: 'maintenance' }))
36
+ })
37
+
38
+ await new Promise<void>(resolve => {
39
+ server.listen(port, resolve)
40
+ })
41
+ t.after(() => {
42
+ server.close()
43
+ })
44
+
45
+ await assert.rejects(
46
+ () => getDBInfo({ couch: `http://localhost:${port}/retryable` }),
47
+ (err: unknown) => {
48
+ assert.ok(err instanceof RetryableError)
49
+ assert.strictEqual(err.statusCode, 503)
50
+ assert.strictEqual(err.message, 'maintenance')
51
+ return true
52
+ }
53
+ )
54
+ })
55
+
56
+ test('converts network failures into RetryableError', async () => {
57
+ await assert.rejects(
58
+ () => getDBInfo({ couch: 'http://localhost:6555/offline-db' }),
59
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
60
+ )
61
+ })
62
+ })