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,280 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import needle from 'needle'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { setTimeout as delay } from 'node:timers/promises'
6
+ import { z } from 'zod'
7
+
8
+ import type { CouchConfigInput } from '../schema/config.mts'
9
+ import { TEST_DB_URL } from '../test/setup-db.mts'
10
+ import { query } from './query.mts'
11
+ import { RetryableError } from './utils/errors.mts'
12
+
13
+ const config: CouchConfigInput = {
14
+ couch: TEST_DB_URL
15
+ }
16
+
17
+ async function putDoc(doc: Record<string, unknown> & { _id: string }) {
18
+ await needle('put', `${TEST_DB_URL}/${doc._id}`, doc, { json: true })
19
+ }
20
+
21
+ async function putDesignDoc(id: string, viewName: string, mapFn: string) {
22
+ await needle(
23
+ 'put',
24
+ `${TEST_DB_URL}/_design/${id}`,
25
+ {
26
+ views: {
27
+ [viewName]: {
28
+ map: mapFn
29
+ }
30
+ }
31
+ },
32
+ { json: true }
33
+ )
34
+ }
35
+
36
+ async function eventually<T>(
37
+ fn: () => Promise<T>,
38
+ predicate: (value: T) => boolean,
39
+ attempts = 10,
40
+ waitMs = 100
41
+ ): Promise<T> {
42
+ let lastValue: T | undefined
43
+ for (let attempt = 0; attempt < attempts; attempt++) {
44
+ lastValue = await fn()
45
+ if (predicate(lastValue)) return lastValue
46
+ await delay(waitMs)
47
+ }
48
+ return lastValue!
49
+ }
50
+
51
+ suite('query', () => {
52
+ test('returns rows with include_docs', async () => {
53
+ const designId = `query-view-${randomUUID()}`
54
+ const viewName = 'byCategory'
55
+ const tag = `query-suite-${randomUUID()}`
56
+ await putDesignDoc(
57
+ designId,
58
+ viewName,
59
+ `function(doc) { if (doc.tag !== '${tag}') return; emit(doc.category, doc.count); }`
60
+ )
61
+
62
+ const matchingDoc = {
63
+ _id: `doc-${randomUUID()}`,
64
+ tag,
65
+ category: 'keep',
66
+ count: 42
67
+ }
68
+ const otherDoc = {
69
+ _id: `doc-${randomUUID()}`,
70
+ tag,
71
+ category: 'skip',
72
+ count: 1
73
+ }
74
+ const unrelatedDoc = {
75
+ _id: `doc-${randomUUID()}`,
76
+ tag: 'other',
77
+ category: 'keep',
78
+ count: 100
79
+ }
80
+
81
+ await putDoc(matchingDoc)
82
+ await putDoc(otherDoc)
83
+ await putDoc(unrelatedDoc)
84
+
85
+ const response = await eventually(
86
+ () =>
87
+ query(config, `_design/${designId}/_view/${viewName}`, {
88
+ include_docs: true,
89
+ key: matchingDoc.category
90
+ }),
91
+ ({ rows }) => rows?.length === 1
92
+ )
93
+
94
+ if (!response.rows) {
95
+ throw new Error('Expected rows in response')
96
+ }
97
+
98
+ assert.strictEqual(response.rows[0].key, matchingDoc.category)
99
+ assert.strictEqual(response.rows[0].value, matchingDoc.count)
100
+ assert.strictEqual(response.rows[0].doc?._id, matchingDoc._id)
101
+ })
102
+
103
+ test('validates rows when schemas provided', async () => {
104
+ const designId = `query-validate-${randomUUID()}`
105
+ const viewName = 'byPlayer'
106
+ const tag = `query-suite-${randomUUID()}`
107
+ await putDesignDoc(
108
+ designId,
109
+ viewName,
110
+ `function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
111
+ )
112
+
113
+ const doc = { _id: `doc-${randomUUID()}`, tag, player: 'alpha', score: 7 }
114
+ await putDoc(doc)
115
+
116
+ const response = await eventually(
117
+ () =>
118
+ query(config, `_design/${designId}/_view/${viewName}`, {
119
+ include_docs: true,
120
+ key: doc.player,
121
+ validate: {
122
+ docSchema: z.looseObject({
123
+ _id: z.string(),
124
+ tag: z.string(),
125
+ player: z.string(),
126
+ score: z.number()
127
+ }),
128
+ keySchema: z.string(),
129
+ valueSchema: z.number()
130
+ }
131
+ }),
132
+ ({ rows }) => rows.length === 1
133
+ )
134
+
135
+ assert.strictEqual(response.rows[0]?.value, doc.score)
136
+ assert.strictEqual(response.rows[0]?.doc?.player, doc.player)
137
+ })
138
+
139
+ test('rejects when validation fails', async () => {
140
+ const designId = `query-invalid-${randomUUID()}`
141
+ const viewName = 'byPlayer'
142
+ const tag = `query-suite-${randomUUID()}`
143
+ await putDesignDoc(
144
+ designId,
145
+ viewName,
146
+ `function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
147
+ )
148
+
149
+ const validDoc = {
150
+ _id: `doc-${randomUUID()}`,
151
+ tag,
152
+ player: 'valid',
153
+ score: 3
154
+ }
155
+ const invalidDoc = {
156
+ _id: `doc-${randomUUID()}`,
157
+ tag,
158
+ player: 'invalid',
159
+ score: 'nope'
160
+ }
161
+
162
+ await putDoc(validDoc)
163
+ await putDoc(invalidDoc)
164
+
165
+ await eventually(
166
+ () =>
167
+ query(config, `_design/${designId}/_view/${viewName}`, {
168
+ key: validDoc.player
169
+ }),
170
+ ({ rows }) => rows?.length === 1
171
+ )
172
+
173
+ await assert.rejects(async () => {
174
+ return query(config, `_design/${designId}/_view/${viewName}`, {
175
+ validate: {
176
+ valueSchema: z.number()
177
+ }
178
+ })
179
+ })
180
+ })
181
+
182
+ test('skips invalid documents when onInvalidDoc=skip', async () => {
183
+ const designId = `query-skip-${randomUUID()}`
184
+ const viewName = 'byPlayer'
185
+ const tag = `query-suite-${randomUUID()}`
186
+ await putDesignDoc(
187
+ designId,
188
+ viewName,
189
+ `function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
190
+ )
191
+
192
+ const validDoc = {
193
+ _id: `doc-${randomUUID()}`,
194
+ tag,
195
+ player: 'valid',
196
+ score: 5
197
+ }
198
+ const invalidDoc = {
199
+ _id: `doc-${randomUUID()}`,
200
+ tag,
201
+ player: 'invalid',
202
+ score: 'nope'
203
+ }
204
+
205
+ await putDoc(validDoc)
206
+ await putDoc(invalidDoc)
207
+
208
+ const response = await eventually(
209
+ () =>
210
+ query(config, `_design/${designId}/_view/${viewName}`, {
211
+ include_docs: true,
212
+ validate: {
213
+ docSchema: z.looseObject({
214
+ _id: z.string(),
215
+ tag: z.string(),
216
+ player: z.string(),
217
+ score: z.number()
218
+ }),
219
+ keySchema: z.string(),
220
+ valueSchema: z.number(),
221
+ onInvalidDoc: 'skip'
222
+ }
223
+ }),
224
+ ({ rows }) => rows.length === 1
225
+ )
226
+
227
+ assert.strictEqual(response.rows[0]?.doc?.player, validDoc.player)
228
+ })
229
+
230
+ test('posts payload when keys exceed URL limit', async () => {
231
+ const designId = `query-post-${randomUUID()}`
232
+ const viewName = 'byPlayer'
233
+ const tag = `query-suite-${randomUUID()}`
234
+ await putDesignDoc(
235
+ designId,
236
+ viewName,
237
+ `function(doc) { if (doc.tag !== '${tag}') return; emit(doc.player, doc.score); }`
238
+ )
239
+
240
+ const targetDoc = {
241
+ _id: `doc-${randomUUID()}`,
242
+ tag,
243
+ player: 'target',
244
+ score: 11
245
+ }
246
+ await putDoc(targetDoc)
247
+
248
+ await eventually(
249
+ () =>
250
+ query(config, `_design/${designId}/_view/${viewName}`, {
251
+ key: targetDoc.player
252
+ }),
253
+ ({ rows }) => rows?.length === 1
254
+ )
255
+
256
+ const bulkKeys = Array.from({ length: 400 }, (_, index) => `missing-${index}-${randomUUID()}`)
257
+ bulkKeys.push(targetDoc.player)
258
+
259
+ const response = await query(config, `_design/${designId}/_view/${viewName}`, {
260
+ include_docs: true,
261
+ keys: bulkKeys
262
+ })
263
+
264
+ if (!response.rows) throw new Error('Expected rows in response')
265
+ assert.strictEqual(response.rows.length, 1)
266
+ assert.strictEqual(response.rows[0]?.key, targetDoc.player)
267
+ assert.strictEqual(response.rows[0]?.doc?._id, targetDoc._id)
268
+ })
269
+
270
+ test('throws RetryableError on network failure', async () => {
271
+ const offlineConfig: CouchConfigInput = {
272
+ couch: 'http://localhost:6553/offline-db'
273
+ }
274
+
275
+ await assert.rejects(
276
+ () => query(offlineConfig, '_all_docs', {}),
277
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
278
+ )
279
+ })
280
+ })
@@ -0,0 +1,65 @@
1
+ import needle from 'needle'
2
+ import { createLogger } from './utils/logger.mts'
3
+ import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
4
+ import { RetryableError } from './utils/errors.mts'
5
+ import { CouchPutResponse } from '../schema/couch/couch.output.schema.ts'
6
+ import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
7
+
8
+ export const remove = async (configInput: CouchConfigInput, id: string, rev: string) => {
9
+ const config = CouchConfig.parse(configInput)
10
+ const logger = createLogger(config)
11
+ const url = `${config.couch}/${id}?rev=${rev}`
12
+ const opts = {
13
+ json: true,
14
+ headers: {
15
+ 'Content-Type': 'application/json'
16
+ }
17
+ }
18
+ const mergedOpts = mergeNeedleOpts(config, opts)
19
+
20
+ logger.info(`Deleting document with id: ${id}`)
21
+ let resp
22
+ try {
23
+ resp = await needle('delete', url, null, mergedOpts)
24
+ } catch (err) {
25
+ logger.error('Error during delete operation:', err)
26
+ RetryableError.handleNetworkError(err)
27
+ }
28
+
29
+ if (!resp) {
30
+ logger.error('No response received from delete request')
31
+ throw new RetryableError('no response', 503)
32
+ }
33
+
34
+ let result
35
+ if (typeof resp.body === 'string') {
36
+ try {
37
+ result = JSON.parse(resp.body)
38
+ } catch {
39
+ result = {}
40
+ }
41
+ } else {
42
+ result = resp.body || {}
43
+ }
44
+ result.statusCode = resp.statusCode
45
+
46
+ if (resp.statusCode === 404) {
47
+ logger.warn(`Document not found for deletion: ${id}`)
48
+ result.ok = false
49
+ result.error = 'not_found'
50
+ return CouchPutResponse.parse(result)
51
+ }
52
+
53
+ if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
54
+ logger.warn(`Retryable status code received: ${resp.statusCode}`)
55
+ throw new RetryableError(result.reason || 'retryable error', resp.statusCode)
56
+ }
57
+
58
+ if (resp.statusCode !== 200) {
59
+ logger.error(`Unexpected status code: ${resp.statusCode}`)
60
+ throw new Error(result.reason || 'failed')
61
+ }
62
+
63
+ logger.info(`Successfully deleted document: ${id}`)
64
+ return CouchPutResponse.parse(result)
65
+ }
@@ -0,0 +1,82 @@
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 { remove } from './remove.mts'
6
+ import { RetryableError } from './utils/errors.mts'
7
+ import { TEST_DB_URL } from '../test/setup-db.mts'
8
+
9
+ const baseConfig: CouchConfigInput = {
10
+ couch: TEST_DB_URL
11
+ }
12
+
13
+ type DocBody = Record<string, unknown>
14
+
15
+ async function saveDoc(id: string, body: DocBody) {
16
+ const response = await needle(
17
+ 'put',
18
+ `${TEST_DB_URL}/${id}`,
19
+ {
20
+ _id: id,
21
+ ...body
22
+ },
23
+ { json: true }
24
+ )
25
+
26
+ if (response.statusCode !== 201 && response.statusCode !== 200) {
27
+ throw new Error(`Failed to save document ${id}: ${response.statusCode}`)
28
+ }
29
+
30
+ return response.body as { rev: string }
31
+ }
32
+
33
+ async function getDoc(id: string) {
34
+ return needle('get', `${TEST_DB_URL}/${id}`, null, { json: true })
35
+ }
36
+
37
+ suite('remove', () => {
38
+ test('it should throw if provided config is invalid', async () => {
39
+ await assert.rejects(async () => {
40
+ await remove(
41
+ // @ts-expect-error testing invalid config
42
+ { couch: DB_URL, useConsoleLogger: true, unexpected: true },
43
+ 'doc-invalid-config',
44
+ '1-invalid'
45
+ )
46
+ })
47
+ })
48
+
49
+ test('integration with pouchdb-server', async t => {
50
+ await t.test('removes an existing document', async () => {
51
+ const remove_doc_id = `remove-doc-1-${Date.now()}`
52
+ const { rev } = await saveDoc(remove_doc_id, { kind: 'test', count: 1 })
53
+
54
+ const result = await remove(baseConfig, remove_doc_id, rev)
55
+ assert.strictEqual(result.ok, true)
56
+ assert.strictEqual(result.id, remove_doc_id)
57
+ assert.strictEqual(result.statusCode, 200)
58
+
59
+ const { statusCode, body } = await getDoc(remove_doc_id)
60
+ assert.strictEqual(statusCode, 404)
61
+ assert.strictEqual(body?.error, 'not_found')
62
+ })
63
+
64
+ await t.test('returns not found metadata when document is missing', async () => {
65
+ const result = await remove(baseConfig, 'remove-doc-missing', '1-missing')
66
+ assert.strictEqual(result.ok, false)
67
+ assert.strictEqual(result.error, 'not_found')
68
+ assert.strictEqual(result.statusCode, 404)
69
+ })
70
+
71
+ await t.test('propagates retryable network errors', async () => {
72
+ const offlineConfig: CouchConfigInput = {
73
+ couch: 'http://localhost:6553/offline-remove-db'
74
+ }
75
+
76
+ await assert.rejects(
77
+ () => remove(offlineConfig, 'remove-doc-network', '1-offline'),
78
+ (err: unknown) => err instanceof RetryableError && err.statusCode === 503
79
+ )
80
+ })
81
+ })
82
+ })
package/impl/retry.mts ADDED
@@ -0,0 +1,66 @@
1
+ import { setTimeout } from 'node:timers/promises'
2
+ import { RetryableError } from './utils/errors.mts'
3
+
4
+ /**
5
+ * Settings that control how retry attempts are scheduled.
6
+ */
7
+ export interface RetryOptions {
8
+ /**
9
+ * Maximum number of retry attempts before rethrowing the original error.
10
+ */
11
+ maxRetries?: number
12
+ /**
13
+ * Initial wait duration in milliseconds before attempting the first retry.
14
+ */
15
+ initialDelay?: number
16
+ /**
17
+ * Multiplier applied to the delay after each retry to implement exponential backoff.
18
+ */
19
+ backoffFactor?: number
20
+ /**
21
+ * Upper bound, in milliseconds, for the delay between retries.
22
+ */
23
+ maxDelay?: number
24
+ }
25
+
26
+ type MaybePromise<T> = PromiseLike<T> | T
27
+
28
+ /**
29
+ * Wrap an async-capable function with retry semantics that respect {@link RetryableError}.
30
+ * @typeParam Fn - The function signature to decorate with retry handling.
31
+ * @param fn The function to invoke with retry support.
32
+ * @param options Retry tuning parameters.
33
+ * @returns A function mirroring `fn` that automatically retries on {@link RetryableError}.
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ export function withRetry<Fn extends (...args: any[]) => MaybePromise<any>>(
37
+ fn: Fn,
38
+ options: RetryOptions = {}
39
+ ): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
40
+ const { maxRetries = 3, initialDelay = 1000, backoffFactor = 2, maxDelay = 30000 } = options
41
+
42
+ return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
43
+ let delay = initialDelay
44
+
45
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
46
+ try {
47
+ const result = await fn(...args)
48
+ return result
49
+ } catch (error) {
50
+ if (!(error instanceof RetryableError)) {
51
+ throw error
52
+ }
53
+
54
+ if (attempt === maxRetries) {
55
+ throw error
56
+ }
57
+
58
+ const nextDelay = Math.min(delay, maxDelay)
59
+ await setTimeout(nextDelay)
60
+ delay *= backoffFactor
61
+ }
62
+ }
63
+
64
+ throw new RetryableError('withRetry exhausted retry attempts without resolving the operation')
65
+ }
66
+ }
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { suite } from 'node:test'
3
+ import { withRetry } from './retry.mts'
4
+ import { RetryableError } from './utils/errors.mts'
5
+
6
+ suite('withRetry', () => {
7
+ test('resolves when the wrapped function succeeds without retries', async () => {
8
+ let count = 0
9
+ const fn = async (value: string) => {
10
+ count += 1
11
+ return value
12
+ }
13
+
14
+ const wrapped = withRetry(fn)
15
+ const result = await wrapped('ok')
16
+
17
+ assert.strictEqual(result, 'ok')
18
+ assert.strictEqual(count, 1)
19
+ })
20
+
21
+ test('retries on RetryableError until success', async () => {
22
+ let attempts = 0
23
+ const fn = async () => {
24
+ attempts += 1
25
+ if (attempts < 3) {
26
+ throw new RetryableError('temporary', 503)
27
+ }
28
+ return 'done'
29
+ }
30
+
31
+ const wrapped = withRetry(fn, { initialDelay: 0, maxDelay: 0 })
32
+ const result = await wrapped()
33
+
34
+ assert.strictEqual(result, 'done')
35
+ assert.strictEqual(attempts, 3)
36
+ })
37
+
38
+ test('propagates non retryable errors immediately', async () => {
39
+ let attempts = 0
40
+ const fn = async () => {
41
+ attempts += 1
42
+ throw new Error('fatal')
43
+ }
44
+
45
+ const wrapped = withRetry(fn, { initialDelay: 0, maxDelay: 0 })
46
+
47
+ await assert.rejects(
48
+ () => wrapped(),
49
+ (err: unknown) => {
50
+ return err instanceof Error && !(err instanceof RetryableError) && err.message === 'fatal'
51
+ }
52
+ )
53
+ assert.strictEqual(attempts, 1)
54
+ })
55
+
56
+ test('throws after exceeding the maximum retries', async () => {
57
+ let attempts = 0
58
+ const fn = async () => {
59
+ attempts += 1
60
+ throw new RetryableError('still failing', 503)
61
+ }
62
+
63
+ const wrapped = withRetry(fn, {
64
+ maxRetries: 2,
65
+ initialDelay: 0,
66
+ maxDelay: 0
67
+ })
68
+
69
+ await assert.rejects(
70
+ () => wrapped(),
71
+ (err: unknown) => {
72
+ return err instanceof RetryableError && err.message === 'still failing'
73
+ }
74
+ )
75
+ assert.strictEqual(attempts, 3)
76
+ })
77
+ })