hide-a-bed 5.2.8 → 6.0.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 (258) 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/eslint.config.js +15 -0
  6. package/impl/bindConfig.mts +140 -0
  7. package/impl/bulkGet.mts +256 -0
  8. package/impl/bulkRemove.mts +98 -0
  9. package/impl/bulkSave.mts +286 -0
  10. package/impl/get.mts +137 -0
  11. package/impl/getDBInfo.mts +67 -0
  12. package/impl/patch.mts +134 -0
  13. package/impl/put.mts +56 -0
  14. package/impl/query.mts +224 -0
  15. package/impl/remove.mts +65 -0
  16. package/impl/retry.mts +66 -0
  17. package/impl/stream.mts +143 -0
  18. package/impl/sugar/lock.mts +103 -0
  19. package/impl/sugar/{watch.mjs → watch.mts} +56 -22
  20. package/impl/utils/errors.mts +130 -0
  21. package/impl/utils/logger.mts +62 -0
  22. package/impl/utils/mergeNeedleOpts.mts +16 -0
  23. package/impl/utils/parseRows.mts +117 -0
  24. package/impl/utils/queryBuilder.mts +173 -0
  25. package/impl/utils/queryString.mts +44 -0
  26. package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
  27. package/impl/utils/transactionErrors.mts +71 -0
  28. package/index.mts +82 -0
  29. package/migration_guides/v6.md +70 -0
  30. package/package.json +49 -32
  31. package/schema/config.mts +81 -0
  32. package/schema/couch/couch.input.schema.ts +43 -0
  33. package/schema/couch/couch.output.schema.ts +169 -0
  34. package/schema/sugar/lock.mts +18 -0
  35. package/schema/sugar/watch.mts +14 -0
  36. package/schema/util.mts +8 -0
  37. package/tsconfig.json +10 -4
  38. package/tsdown.config.ts +16 -0
  39. package/typedoc.json +4 -0
  40. package/types/output/eslint.config.d.ts +3 -0
  41. package/types/output/eslint.config.d.ts.map +1 -0
  42. package/types/output/impl/bindConfig.d.mts +174 -0
  43. package/types/output/impl/bindConfig.d.mts.map +1 -0
  44. package/types/output/impl/bulkGet.d.mts +75 -0
  45. package/types/output/impl/bulkGet.d.mts.map +1 -0
  46. package/types/output/impl/bulkGet.test.d.mts +2 -0
  47. package/types/output/impl/bulkGet.test.d.mts.map +1 -0
  48. package/types/output/impl/bulkRemove.d.mts +63 -0
  49. package/types/output/impl/bulkRemove.d.mts.map +1 -0
  50. package/types/output/impl/bulkRemove.test.d.mts +2 -0
  51. package/types/output/impl/bulkRemove.test.d.mts.map +1 -0
  52. package/types/output/impl/bulkSave.d.mts +64 -0
  53. package/types/output/impl/bulkSave.d.mts.map +1 -0
  54. package/types/output/impl/bulkSave.test.d.mts +2 -0
  55. package/types/output/impl/bulkSave.test.d.mts.map +1 -0
  56. package/types/output/impl/get.d.mts +20 -0
  57. package/types/output/impl/get.d.mts.map +1 -0
  58. package/types/output/impl/get.test.d.mts +2 -0
  59. package/types/output/impl/get.test.d.mts.map +1 -0
  60. package/types/output/impl/getDBInfo.d.mts +52 -0
  61. package/types/output/impl/getDBInfo.d.mts.map +1 -0
  62. package/types/output/impl/getDBInfo.test.d.mts +2 -0
  63. package/types/output/impl/getDBInfo.test.d.mts.map +1 -0
  64. package/types/output/impl/patch.d.mts +45 -0
  65. package/types/output/impl/patch.d.mts.map +1 -0
  66. package/types/output/impl/patch.test.d.mts +2 -0
  67. package/types/output/impl/patch.test.d.mts.map +1 -0
  68. package/types/output/impl/put.d.mts +5 -0
  69. package/types/output/impl/put.d.mts.map +1 -0
  70. package/types/output/impl/put.test.d.mts +2 -0
  71. package/types/output/impl/put.test.d.mts.map +1 -0
  72. package/types/output/impl/query.d.mts +47 -0
  73. package/types/output/impl/query.d.mts.map +1 -0
  74. package/types/output/impl/query.test.d.mts +2 -0
  75. package/types/output/impl/query.test.d.mts.map +1 -0
  76. package/types/output/impl/remove.d.mts +9 -0
  77. package/types/output/impl/remove.d.mts.map +1 -0
  78. package/types/output/impl/remove.test.d.mts +2 -0
  79. package/types/output/impl/remove.test.d.mts.map +1 -0
  80. package/types/output/impl/retry.d.mts +32 -0
  81. package/types/output/impl/retry.d.mts.map +1 -0
  82. package/types/output/impl/retry.test.d.mts +2 -0
  83. package/types/output/impl/retry.test.d.mts.map +1 -0
  84. package/types/output/impl/stream.d.mts +13 -0
  85. package/types/output/impl/stream.d.mts.map +1 -0
  86. package/types/output/impl/stream.test.d.mts +2 -0
  87. package/types/output/impl/stream.test.d.mts.map +1 -0
  88. package/types/output/impl/sugar/lock.d.mts +24 -0
  89. package/types/output/impl/sugar/lock.d.mts.map +1 -0
  90. package/types/output/impl/sugar/lock.test.d.mts +2 -0
  91. package/types/output/impl/sugar/lock.test.d.mts.map +1 -0
  92. package/types/output/impl/sugar/watch.d.mts +21 -0
  93. package/types/output/impl/sugar/watch.d.mts.map +1 -0
  94. package/types/output/impl/sugar/watch.test.d.mts +2 -0
  95. package/types/output/impl/sugar/watch.test.d.mts.map +1 -0
  96. package/types/output/impl/utils/errors.d.mts +78 -0
  97. package/types/output/impl/utils/errors.d.mts.map +1 -0
  98. package/types/output/impl/utils/errors.test.d.mts +2 -0
  99. package/types/output/impl/utils/errors.test.d.mts.map +1 -0
  100. package/types/output/impl/utils/logger.d.mts +11 -0
  101. package/types/output/impl/utils/logger.d.mts.map +1 -0
  102. package/types/output/impl/utils/logger.test.d.mts +2 -0
  103. package/types/output/impl/utils/logger.test.d.mts.map +1 -0
  104. package/types/output/impl/utils/mergeNeedleOpts.d.mts +53 -0
  105. package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +1 -0
  106. package/types/output/impl/utils/parseRows.d.mts +15 -0
  107. package/types/output/impl/utils/parseRows.d.mts.map +1 -0
  108. package/types/output/impl/utils/parseRows.test.d.mts +2 -0
  109. package/types/output/impl/utils/parseRows.test.d.mts.map +1 -0
  110. package/types/output/impl/utils/queryBuilder.d.mts +68 -0
  111. package/types/output/impl/utils/queryBuilder.d.mts.map +1 -0
  112. package/types/output/impl/utils/queryBuilder.test.d.mts +2 -0
  113. package/types/output/impl/utils/queryBuilder.test.d.mts.map +1 -0
  114. package/types/output/impl/utils/queryString.d.mts +9 -0
  115. package/types/output/impl/utils/queryString.d.mts.map +1 -0
  116. package/types/output/impl/utils/queryString.test.d.mts +2 -0
  117. package/types/output/impl/utils/queryString.test.d.mts.map +1 -0
  118. package/types/output/impl/utils/trackedEmitter.d.mts +7 -0
  119. package/types/output/impl/utils/trackedEmitter.d.mts.map +1 -0
  120. package/{impl → types/output/impl/utils}/transactionErrors.d.mts +16 -31
  121. package/types/output/impl/utils/transactionErrors.d.mts.map +1 -0
  122. package/types/output/index.d.mts +32 -0
  123. package/types/output/index.d.mts.map +1 -0
  124. package/types/output/index.test.d.mts +2 -0
  125. package/types/output/index.test.d.mts.map +1 -0
  126. package/types/output/schema/config.d.mts +90 -0
  127. package/types/output/schema/config.d.mts.map +1 -0
  128. package/types/output/schema/couch/couch.input.schema.d.ts +29 -0
  129. package/types/output/schema/couch/couch.input.schema.d.ts.map +1 -0
  130. package/types/output/schema/couch/couch.output.schema.d.ts +113 -0
  131. package/types/output/schema/couch/couch.output.schema.d.ts.map +1 -0
  132. package/types/output/schema/sugar/lock.d.mts +19 -0
  133. package/types/output/schema/sugar/lock.d.mts.map +1 -0
  134. package/types/output/schema/sugar/watch.d.mts +11 -0
  135. package/types/output/schema/sugar/watch.d.mts.map +1 -0
  136. package/types/output/schema/util.d.mts +85 -0
  137. package/types/output/schema/util.d.mts.map +1 -0
  138. package/types/output/tsdown.config.d.ts +3 -0
  139. package/types/output/tsdown.config.d.ts.map +1 -0
  140. package/types/output/types/standard-schema.d.ts +60 -0
  141. package/types/output/types/standard-schema.d.ts.map +1 -0
  142. package/types/standard-schema.ts +76 -0
  143. package/types/utils.d.ts +1 -0
  144. package/cjs/impl/bulk.cjs +0 -275
  145. package/cjs/impl/changes.cjs +0 -67
  146. package/cjs/impl/crud.cjs +0 -127
  147. package/cjs/impl/errors.cjs +0 -75
  148. package/cjs/impl/logger.cjs +0 -70
  149. package/cjs/impl/patch.cjs +0 -95
  150. package/cjs/impl/query.cjs +0 -116
  151. package/cjs/impl/queryBuilder.cjs +0 -163
  152. package/cjs/impl/retry.cjs +0 -55
  153. package/cjs/impl/stream.cjs +0 -121
  154. package/cjs/impl/sugar/lock.cjs +0 -81
  155. package/cjs/impl/sugar/watch.cjs +0 -159
  156. package/cjs/impl/trackedEmitter.cjs +0 -54
  157. package/cjs/impl/transactionErrors.cjs +0 -70
  158. package/cjs/impl/util.cjs +0 -64
  159. package/cjs/index.cjs +0 -132
  160. package/cjs/integration/changes.cjs +0 -76
  161. package/cjs/integration/disconnect-watch.cjs +0 -52
  162. package/cjs/integration/watch.cjs +0 -59
  163. package/cjs/schema/bind.cjs +0 -59
  164. package/cjs/schema/bulk.cjs +0 -92
  165. package/cjs/schema/changes.cjs +0 -68
  166. package/cjs/schema/config.cjs +0 -48
  167. package/cjs/schema/crud.cjs +0 -77
  168. package/cjs/schema/patch.cjs +0 -53
  169. package/cjs/schema/query.cjs +0 -62
  170. package/cjs/schema/stream.cjs +0 -42
  171. package/cjs/schema/sugar/lock.cjs +0 -59
  172. package/cjs/schema/sugar/watch.cjs +0 -42
  173. package/cjs/schema/util.cjs +0 -39
  174. package/config.json +0 -5
  175. package/docs/compiler.png +0 -0
  176. package/dualmode.config.json +0 -11
  177. package/impl/bulk.d.mts +0 -11
  178. package/impl/bulk.d.mts.map +0 -1
  179. package/impl/bulk.mjs +0 -291
  180. package/impl/changes.d.mts +0 -12
  181. package/impl/changes.d.mts.map +0 -1
  182. package/impl/changes.mjs +0 -53
  183. package/impl/crud.d.mts +0 -7
  184. package/impl/crud.d.mts.map +0 -1
  185. package/impl/crud.mjs +0 -108
  186. package/impl/errors.d.mts +0 -43
  187. package/impl/errors.d.mts.map +0 -1
  188. package/impl/errors.mjs +0 -65
  189. package/impl/logger.d.mts +0 -32
  190. package/impl/logger.d.mts.map +0 -1
  191. package/impl/logger.mjs +0 -59
  192. package/impl/patch.d.mts +0 -6
  193. package/impl/patch.d.mts.map +0 -1
  194. package/impl/patch.mjs +0 -88
  195. package/impl/query.d.mts +0 -195
  196. package/impl/query.d.mts.map +0 -1
  197. package/impl/query.mjs +0 -122
  198. package/impl/queryBuilder.d.mts +0 -154
  199. package/impl/queryBuilder.d.mts.map +0 -1
  200. package/impl/queryBuilder.mjs +0 -175
  201. package/impl/retry.d.mts +0 -2
  202. package/impl/retry.d.mts.map +0 -1
  203. package/impl/retry.mjs +0 -39
  204. package/impl/stream.d.mts +0 -3
  205. package/impl/stream.d.mts.map +0 -1
  206. package/impl/stream.mjs +0 -98
  207. package/impl/sugar/lock.d.mts +0 -5
  208. package/impl/sugar/lock.d.mts.map +0 -1
  209. package/impl/sugar/lock.mjs +0 -70
  210. package/impl/sugar/watch.d.mts +0 -34
  211. package/impl/sugar/watch.d.mts.map +0 -1
  212. package/impl/trackedEmitter.d.mts +0 -8
  213. package/impl/trackedEmitter.d.mts.map +0 -1
  214. package/impl/transactionErrors.d.mts.map +0 -1
  215. package/impl/transactionErrors.mjs +0 -47
  216. package/impl/util.d.mts +0 -3
  217. package/impl/util.d.mts.map +0 -1
  218. package/impl/util.mjs +0 -35
  219. package/index.d.mts +0 -80
  220. package/index.d.mts.map +0 -1
  221. package/index.mjs +0 -141
  222. package/integration/changes.mjs +0 -60
  223. package/integration/disconnect-watch.mjs +0 -36
  224. package/integration/watch.mjs +0 -40
  225. package/schema/bind.d.mts +0 -5461
  226. package/schema/bind.d.mts.map +0 -1
  227. package/schema/bind.mjs +0 -43
  228. package/schema/bulk.d.mts +0 -923
  229. package/schema/bulk.d.mts.map +0 -1
  230. package/schema/bulk.mjs +0 -83
  231. package/schema/changes.d.mts +0 -191
  232. package/schema/changes.d.mts.map +0 -1
  233. package/schema/changes.mjs +0 -59
  234. package/schema/config.d.mts +0 -79
  235. package/schema/config.d.mts.map +0 -1
  236. package/schema/config.mjs +0 -26
  237. package/schema/crud.d.mts +0 -491
  238. package/schema/crud.d.mts.map +0 -1
  239. package/schema/crud.mjs +0 -64
  240. package/schema/patch.d.mts +0 -255
  241. package/schema/patch.d.mts.map +0 -1
  242. package/schema/patch.mjs +0 -42
  243. package/schema/query.d.mts +0 -406
  244. package/schema/query.d.mts.map +0 -1
  245. package/schema/query.mjs +0 -45
  246. package/schema/stream.d.mts +0 -211
  247. package/schema/stream.d.mts.map +0 -1
  248. package/schema/stream.mjs +0 -23
  249. package/schema/sugar/lock.d.mts +0 -238
  250. package/schema/sugar/lock.d.mts.map +0 -1
  251. package/schema/sugar/lock.mjs +0 -50
  252. package/schema/sugar/watch.d.mts +0 -127
  253. package/schema/sugar/watch.d.mts.map +0 -1
  254. package/schema/sugar/watch.mjs +0 -29
  255. package/schema/util.d.mts +0 -160
  256. package/schema/util.d.mts.map +0 -1
  257. package/schema/util.mjs +0 -35
  258. package/types/changes-stream.d.ts +0 -11
@@ -1,18 +1,43 @@
1
1
  import needle from 'needle'
2
2
  import { EventEmitter } from 'events'
3
- import { RetryableError } from '../errors.mjs'
4
- import { createLogger } from '../logger.mjs'
5
- import { sleep } from '../patch.mjs'
6
- import { WatchDocs } from '../../schema/sugar/watch.mjs'
7
-
8
- // watch the doc for any changes
9
- export const watchDocs = WatchDocs.implement((config, docIds, onChange, options = {}) => {
3
+ import { RetryableError } from '../utils/errors.mts'
4
+ import { createLogger } from '../utils/logger.mts'
5
+ import { WatchOptions, type WatchOptionsInput } from '../../schema/sugar/watch.mts'
6
+ import { mergeNeedleOpts } from '../utils/mergeNeedleOpts.mts'
7
+ import { setTimeout } from 'node:timers/promises'
8
+ import {
9
+ CouchConfig,
10
+ type CouchConfigInput,
11
+ type NeedleBaseOptionsSchema
12
+ } from '../../schema/config.mts'
13
+
14
+ /**
15
+ * Watch for changes to specified document IDs in CouchDB.
16
+ * Calls the onChange callback for each change detected.
17
+ * Returns an emitter with methods to listen for events and stop watching.
18
+ *
19
+ * @param configInput CouchDB configuration
20
+ * @param docIds Document ID or array of document IDs to watch
21
+ * @param onChange Callback function called on each change
22
+ * @param optionsInput Watch options
23
+ *
24
+ * @return WatchEmitter with methods to manage the watch
25
+ */
26
+ export function watchDocs(
27
+ configInput: CouchConfigInput,
28
+ docIds: string | string[],
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ onChange: (change: any) => void,
31
+ optionsInput: WatchOptionsInput = {}
32
+ ) {
33
+ const config = CouchConfig.parse(configInput)
34
+ const options = WatchOptions.parse(optionsInput)
10
35
  const logger = createLogger(config)
11
36
  const emitter = new EventEmitter()
12
- let lastSeq = null || 'now'
37
+ let lastSeq: null | 'now' = null
13
38
  let stopping = false
14
39
  let retryCount = 0
15
- let currentRequest = null
40
+ let currentRequest: null | ReturnType<typeof needle.get> = null
16
41
  const maxRetries = options.maxRetries || 10
17
42
  const initialDelay = options.initialDelay || 1000
18
43
  const maxDelay = options.maxDelay || 30000
@@ -29,13 +54,17 @@ export const watchDocs = WatchDocs.implement((config, docIds, onChange, options
29
54
  const ids = _docIds.join('","')
30
55
  const url = `${config.couch}/_changes?feed=${feed}&since=${lastSeq}&include_docs=${includeDocs}&filter=_doc_ids&doc_ids=["${ids}"]`
31
56
 
32
- const opts = {
33
- headers: { 'Content-Type': 'application/json' },
57
+ const opts: NeedleBaseOptionsSchema = {
58
+ json: false,
59
+ headers: {
60
+ 'Content-Type': 'application/json'
61
+ },
34
62
  parse_response: false
35
63
  }
64
+ const mergedOpts = mergeNeedleOpts(config, opts)
36
65
 
37
66
  let buffer = ''
38
- currentRequest = needle.get(url, opts)
67
+ currentRequest = needle.get(url, mergedOpts)
39
68
 
40
69
  currentRequest.on('data', chunk => {
41
70
  buffer += chunk.toString()
@@ -61,13 +90,16 @@ export const watchDocs = WatchDocs.implement((config, docIds, onChange, options
61
90
  })
62
91
 
63
92
  currentRequest.on('response', response => {
64
- logger.debug(`Received response with status code, watching [${_docIds}]: ${response.statusCode}`)
93
+ logger.debug(
94
+ `Received response with status code, watching [${_docIds}]: ${response.statusCode}`
95
+ )
65
96
  if (RetryableError.isRetryableStatusCode(response.statusCode)) {
66
97
  logger.warn(`Retryable status code received: ${response.statusCode}`)
67
- currentRequest.abort()
98
+ // @ts-expect-error bad type?
99
+ currentRequest?.destroy()
68
100
  handleReconnect()
69
101
  } else {
70
- // Reset retry count on successful connection
102
+ // Reset retry count on successful connection
71
103
  retryCount = 0
72
104
  }
73
105
  })
@@ -85,14 +117,14 @@ export const watchDocs = WatchDocs.implement((config, docIds, onChange, options
85
117
  logger.info(`Retryable error, watching [${_docIds}]:`, filteredError.toString())
86
118
  handleReconnect()
87
119
  } else {
88
- logger.error(`Non-retryable error, watching [${_docIds}]`, filteredError.toString())
120
+ logger.error(`Non-retryable error, watching [${_docIds}]`, filteredError?.toString())
89
121
  emitter.emit('error', filteredError)
90
122
  }
91
123
  }
92
124
  })
93
125
 
94
126
  currentRequest.on('end', () => {
95
- // Process any remaining data in buffer
127
+ // Process any remaining data in buffer
96
128
  if (buffer.trim()) {
97
129
  try {
98
130
  const change = JSON.parse(buffer)
@@ -125,7 +157,7 @@ export const watchDocs = WatchDocs.implement((config, docIds, onChange, options
125
157
  retryCount++
126
158
 
127
159
  logger.info(`Attempting to reconnect in ${delay}ms (attempt ${retryCount} of ${maxRetries})`)
128
- await sleep(delay)
160
+ await setTimeout(delay)
129
161
 
130
162
  try {
131
163
  connect()
@@ -142,13 +174,15 @@ export const watchDocs = WatchDocs.implement((config, docIds, onChange, options
142
174
  emitter.on('change', onChange)
143
175
 
144
176
  return {
145
- on: (event, listener) => emitter.on(event, listener),
146
- removeListener: (event, listener) => emitter.removeListener(event, listener),
177
+ on: (event: string, listener: EventListener) => emitter.on(event, listener),
178
+ removeListener: (event: string, listener: EventListener) =>
179
+ emitter.removeListener(event, listener),
147
180
  stop: () => {
148
181
  stopping = true
149
- if (currentRequest) currentRequest.abort()
182
+ // @ts-expect-error bad type?
183
+ if (currentRequest) currentRequest.destroy()
150
184
  emitter.emit('end', { lastSeq })
151
185
  emitter.removeAllListeners()
152
186
  }
153
187
  }
154
- })
188
+ }
@@ -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,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,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
+ }
@@ -0,0 +1,173 @@
1
+ import type { ViewOptions } from '../../schema/couch/couch.input.schema.ts'
2
+
3
+ /**
4
+ * A builder class for constructing CouchDB view query options.
5
+ * Provides a fluent API for setting various query parameters.
6
+ * @example
7
+ * const queryOptions = new QueryBuilder()
8
+ * .limit(10)
9
+ * .include_docs()
10
+ * .startKey('someKey')
11
+ * .build();
12
+ * @see SimpleViewOptions for the full list of options.
13
+ *
14
+ * @remarks
15
+ * Each method corresponds to a CouchDB view option and returns the builder instance for chaining.
16
+ *
17
+ * @returns The constructed SimpleViewOptions object.
18
+ */
19
+ export class QueryBuilder {
20
+ #options: ViewOptions = {}
21
+
22
+ descending(descending = true): this {
23
+ this.#options.descending = descending
24
+ return this
25
+ }
26
+
27
+ endkey_docid(endkeyDocId: NonNullable<ViewOptions['endkey_docid']>): this {
28
+ this.#options.endkey_docid = endkeyDocId
29
+ return this
30
+ }
31
+
32
+ /**
33
+ * Alias for endkey_docid
34
+ */
35
+ end_key_doc_id(endkeyDocId: NonNullable<ViewOptions['endkey_docid']>): this {
36
+ this.#options.endkey_docid = endkeyDocId
37
+ return this
38
+ }
39
+
40
+ endkey(endkey: ViewOptions['endkey']): this {
41
+ this.#options.endkey = endkey
42
+ return this
43
+ }
44
+
45
+ /**
46
+ * Alias for endkey
47
+ */
48
+ endKey(endkey: ViewOptions['endkey']): this {
49
+ this.#options.endkey = endkey
50
+ return this
51
+ }
52
+
53
+ /**
54
+ * Alias for endkey
55
+ */
56
+ end_key(endkey: ViewOptions['endkey']): this {
57
+ this.#options.endkey = endkey
58
+ return this
59
+ }
60
+
61
+ group(group = true): this {
62
+ this.#options.group = group
63
+ return this
64
+ }
65
+
66
+ group_level(level: NonNullable<ViewOptions['group_level']>): this {
67
+ this.#options.group_level = level
68
+ return this
69
+ }
70
+
71
+ include_docs(includeDocs = true): this {
72
+ this.#options.include_docs = includeDocs
73
+ return this
74
+ }
75
+
76
+ inclusive_end(inclusiveEnd = true): this {
77
+ this.#options.inclusive_end = inclusiveEnd
78
+ return this
79
+ }
80
+
81
+ key(key: ViewOptions['key']): this {
82
+ this.#options.key = key
83
+ return this
84
+ }
85
+
86
+ keys(keys: NonNullable<ViewOptions['keys']>): this {
87
+ this.#options.keys = keys
88
+ return this
89
+ }
90
+
91
+ limit(limit: NonNullable<ViewOptions['limit']>): this {
92
+ this.#options.limit = limit
93
+ return this
94
+ }
95
+
96
+ reduce(reduce = true): this {
97
+ this.#options.reduce = reduce
98
+ return this
99
+ }
100
+
101
+ skip(skip: NonNullable<ViewOptions['skip']>): this {
102
+ this.#options.skip = skip
103
+ return this
104
+ }
105
+
106
+ sorted(sorted = true): this {
107
+ this.#options.sorted = sorted
108
+ return this
109
+ }
110
+
111
+ stable(stable = true): this {
112
+ this.#options.stable = stable
113
+ return this
114
+ }
115
+
116
+ startkey(startkey: ViewOptions['startkey']): this {
117
+ this.#options.startkey = startkey
118
+ return this
119
+ }
120
+
121
+ /**
122
+ * Alias for startkey
123
+ */
124
+ startKey(startkey: ViewOptions['startkey']): this {
125
+ this.#options.startkey = startkey
126
+ return this
127
+ }
128
+
129
+ /**
130
+ * Alias for startkey
131
+ */
132
+ start_key(startkey: ViewOptions['startkey']): this {
133
+ this.#options.startkey = startkey
134
+ return this
135
+ }
136
+
137
+ startkey_docid(startkeyDocId: NonNullable<ViewOptions['startkey_docid']>): this {
138
+ this.#options.startkey_docid = startkeyDocId
139
+ return this
140
+ }
141
+
142
+ /**
143
+ * Alias for startkey_docid
144
+ */
145
+ start_key_doc_id(startkeyDocId: NonNullable<ViewOptions['startkey_docid']>): this {
146
+ this.#options.startkey_docid = startkeyDocId
147
+ return this
148
+ }
149
+
150
+ update(update: NonNullable<ViewOptions['update']>): this {
151
+ this.#options.update = update
152
+ return this
153
+ }
154
+
155
+ update_seq(updateSeq = true): this {
156
+ this.#options.update_seq = updateSeq
157
+ return this
158
+ }
159
+
160
+ /**
161
+ * Builds and returns the ViewOptions object.
162
+ */
163
+ build(): ViewOptions {
164
+ return { ...this.#options }
165
+ }
166
+ }
167
+
168
+ type AssertViewOptionsCovered =
169
+ Exclude<keyof ViewOptions, keyof QueryBuilder> extends never ? true : never
170
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
171
+ const _assertViewOptionsCovered: AssertViewOptionsCovered = true
172
+
173
+ export const createQuery = (): QueryBuilder => new QueryBuilder()
@@ -0,0 +1,44 @@
1
+ import { ViewOptions } from '../../schema/couch/couch.input.schema.ts'
2
+
3
+ const KEYS_TO_QUOTE: (keyof ViewOptions)[] = [
4
+ 'endkey_docid',
5
+ 'endkey',
6
+ 'key',
7
+ 'keys',
8
+ 'startkey',
9
+ 'startkey_docid',
10
+ 'update'
11
+ ]
12
+
13
+ /**
14
+ * Serialize CouchDB view options into a URL-safe query string, quoting values CouchDB expects as JSON.
15
+ * @param options The view options to serialize
16
+ * @param params The list of option keys that require JSON quoting
17
+ * @returns The serialized query string
18
+ */
19
+ export function queryString(options: ViewOptions = {}): string {
20
+ const searchParams = new URLSearchParams()
21
+ const parsedOptions = ViewOptions.parse(options)
22
+ Object.entries(parsedOptions).forEach(([key, rawValue]) => {
23
+ let value = rawValue
24
+ if (KEYS_TO_QUOTE.includes(key as keyof ViewOptions)) {
25
+ if (typeof value === 'string') value = `"${value}"`
26
+ if (Array.isArray(value)) {
27
+ value =
28
+ '[' +
29
+ value
30
+ .map(i => {
31
+ if (i === null) return 'null'
32
+ if (typeof i === 'string') return `"${i}"`
33
+ if (typeof i === 'object' && Object.keys(i).length === 0) return '{}'
34
+ if (typeof i === 'object') return JSON.stringify(i)
35
+ return i
36
+ })
37
+ .join(',') +
38
+ ']'
39
+ }
40
+ }
41
+ searchParams.set(key, String(value))
42
+ })
43
+ return searchParams.toString()
44
+ }