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.
- package/.prettierrc +7 -0
- package/README.md +270 -218
- package/dist/cjs/index.cjs +1952 -0
- package/dist/esm/index.mjs +1898 -0
- package/eslint.config.js +15 -0
- package/impl/bindConfig.mts +140 -0
- package/impl/bulkGet.mts +256 -0
- package/impl/bulkRemove.mts +98 -0
- package/impl/bulkSave.mts +286 -0
- package/impl/get.mts +137 -0
- package/impl/getDBInfo.mts +67 -0
- package/impl/patch.mts +134 -0
- package/impl/put.mts +56 -0
- package/impl/query.mts +224 -0
- package/impl/remove.mts +65 -0
- package/impl/retry.mts +66 -0
- package/impl/stream.mts +143 -0
- package/impl/sugar/lock.mts +103 -0
- package/impl/sugar/{watch.mjs → watch.mts} +56 -22
- package/impl/utils/errors.mts +130 -0
- package/impl/utils/logger.mts +62 -0
- package/impl/utils/mergeNeedleOpts.mts +16 -0
- package/impl/utils/parseRows.mts +117 -0
- package/impl/utils/queryBuilder.mts +173 -0
- package/impl/utils/queryString.mts +44 -0
- package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
- package/impl/utils/transactionErrors.mts +71 -0
- package/index.mts +82 -0
- package/migration_guides/v6.md +70 -0
- package/package.json +49 -32
- package/schema/config.mts +81 -0
- package/schema/couch/couch.input.schema.ts +43 -0
- package/schema/couch/couch.output.schema.ts +169 -0
- package/schema/sugar/lock.mts +18 -0
- package/schema/sugar/watch.mts +14 -0
- package/schema/util.mts +8 -0
- package/tsconfig.json +10 -4
- package/tsdown.config.ts +16 -0
- package/typedoc.json +4 -0
- package/types/output/eslint.config.d.ts +3 -0
- package/types/output/eslint.config.d.ts.map +1 -0
- package/types/output/impl/bindConfig.d.mts +174 -0
- package/types/output/impl/bindConfig.d.mts.map +1 -0
- package/types/output/impl/bulkGet.d.mts +75 -0
- package/types/output/impl/bulkGet.d.mts.map +1 -0
- package/types/output/impl/bulkGet.test.d.mts +2 -0
- package/types/output/impl/bulkGet.test.d.mts.map +1 -0
- package/types/output/impl/bulkRemove.d.mts +63 -0
- package/types/output/impl/bulkRemove.d.mts.map +1 -0
- package/types/output/impl/bulkRemove.test.d.mts +2 -0
- package/types/output/impl/bulkRemove.test.d.mts.map +1 -0
- package/types/output/impl/bulkSave.d.mts +64 -0
- package/types/output/impl/bulkSave.d.mts.map +1 -0
- package/types/output/impl/bulkSave.test.d.mts +2 -0
- package/types/output/impl/bulkSave.test.d.mts.map +1 -0
- package/types/output/impl/get.d.mts +20 -0
- package/types/output/impl/get.d.mts.map +1 -0
- package/types/output/impl/get.test.d.mts +2 -0
- package/types/output/impl/get.test.d.mts.map +1 -0
- package/types/output/impl/getDBInfo.d.mts +52 -0
- package/types/output/impl/getDBInfo.d.mts.map +1 -0
- package/types/output/impl/getDBInfo.test.d.mts +2 -0
- package/types/output/impl/getDBInfo.test.d.mts.map +1 -0
- package/types/output/impl/patch.d.mts +45 -0
- package/types/output/impl/patch.d.mts.map +1 -0
- package/types/output/impl/patch.test.d.mts +2 -0
- package/types/output/impl/patch.test.d.mts.map +1 -0
- package/types/output/impl/put.d.mts +5 -0
- package/types/output/impl/put.d.mts.map +1 -0
- package/types/output/impl/put.test.d.mts +2 -0
- package/types/output/impl/put.test.d.mts.map +1 -0
- package/types/output/impl/query.d.mts +47 -0
- package/types/output/impl/query.d.mts.map +1 -0
- package/types/output/impl/query.test.d.mts +2 -0
- package/types/output/impl/query.test.d.mts.map +1 -0
- package/types/output/impl/remove.d.mts +9 -0
- package/types/output/impl/remove.d.mts.map +1 -0
- package/types/output/impl/remove.test.d.mts +2 -0
- package/types/output/impl/remove.test.d.mts.map +1 -0
- package/types/output/impl/retry.d.mts +32 -0
- package/types/output/impl/retry.d.mts.map +1 -0
- package/types/output/impl/retry.test.d.mts +2 -0
- package/types/output/impl/retry.test.d.mts.map +1 -0
- package/types/output/impl/stream.d.mts +13 -0
- package/types/output/impl/stream.d.mts.map +1 -0
- package/types/output/impl/stream.test.d.mts +2 -0
- package/types/output/impl/stream.test.d.mts.map +1 -0
- package/types/output/impl/sugar/lock.d.mts +24 -0
- package/types/output/impl/sugar/lock.d.mts.map +1 -0
- package/types/output/impl/sugar/lock.test.d.mts +2 -0
- package/types/output/impl/sugar/lock.test.d.mts.map +1 -0
- package/types/output/impl/sugar/watch.d.mts +21 -0
- package/types/output/impl/sugar/watch.d.mts.map +1 -0
- package/types/output/impl/sugar/watch.test.d.mts +2 -0
- package/types/output/impl/sugar/watch.test.d.mts.map +1 -0
- package/types/output/impl/utils/errors.d.mts +78 -0
- package/types/output/impl/utils/errors.d.mts.map +1 -0
- package/types/output/impl/utils/errors.test.d.mts +2 -0
- package/types/output/impl/utils/errors.test.d.mts.map +1 -0
- package/types/output/impl/utils/logger.d.mts +11 -0
- package/types/output/impl/utils/logger.d.mts.map +1 -0
- package/types/output/impl/utils/logger.test.d.mts +2 -0
- package/types/output/impl/utils/logger.test.d.mts.map +1 -0
- package/types/output/impl/utils/mergeNeedleOpts.d.mts +53 -0
- package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.d.mts +15 -0
- package/types/output/impl/utils/parseRows.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.test.d.mts +2 -0
- package/types/output/impl/utils/parseRows.test.d.mts.map +1 -0
- package/types/output/impl/utils/queryBuilder.d.mts +68 -0
- package/types/output/impl/utils/queryBuilder.d.mts.map +1 -0
- package/types/output/impl/utils/queryBuilder.test.d.mts +2 -0
- package/types/output/impl/utils/queryBuilder.test.d.mts.map +1 -0
- package/types/output/impl/utils/queryString.d.mts +9 -0
- package/types/output/impl/utils/queryString.d.mts.map +1 -0
- package/types/output/impl/utils/queryString.test.d.mts +2 -0
- package/types/output/impl/utils/queryString.test.d.mts.map +1 -0
- package/types/output/impl/utils/trackedEmitter.d.mts +7 -0
- package/types/output/impl/utils/trackedEmitter.d.mts.map +1 -0
- package/{impl → types/output/impl/utils}/transactionErrors.d.mts +16 -31
- package/types/output/impl/utils/transactionErrors.d.mts.map +1 -0
- package/types/output/index.d.mts +32 -0
- package/types/output/index.d.mts.map +1 -0
- package/types/output/index.test.d.mts +2 -0
- package/types/output/index.test.d.mts.map +1 -0
- package/types/output/schema/config.d.mts +90 -0
- package/types/output/schema/config.d.mts.map +1 -0
- package/types/output/schema/couch/couch.input.schema.d.ts +29 -0
- package/types/output/schema/couch/couch.input.schema.d.ts.map +1 -0
- package/types/output/schema/couch/couch.output.schema.d.ts +113 -0
- package/types/output/schema/couch/couch.output.schema.d.ts.map +1 -0
- package/types/output/schema/sugar/lock.d.mts +19 -0
- package/types/output/schema/sugar/lock.d.mts.map +1 -0
- package/types/output/schema/sugar/watch.d.mts +11 -0
- package/types/output/schema/sugar/watch.d.mts.map +1 -0
- package/types/output/schema/util.d.mts +85 -0
- package/types/output/schema/util.d.mts.map +1 -0
- package/types/output/tsdown.config.d.ts +3 -0
- package/types/output/tsdown.config.d.ts.map +1 -0
- package/types/output/types/standard-schema.d.ts +60 -0
- package/types/output/types/standard-schema.d.ts.map +1 -0
- package/types/standard-schema.ts +76 -0
- package/types/utils.d.ts +1 -0
- package/cjs/impl/bulk.cjs +0 -275
- package/cjs/impl/changes.cjs +0 -67
- package/cjs/impl/crud.cjs +0 -127
- package/cjs/impl/errors.cjs +0 -75
- package/cjs/impl/logger.cjs +0 -70
- package/cjs/impl/patch.cjs +0 -95
- package/cjs/impl/query.cjs +0 -116
- package/cjs/impl/queryBuilder.cjs +0 -163
- package/cjs/impl/retry.cjs +0 -55
- package/cjs/impl/stream.cjs +0 -121
- package/cjs/impl/sugar/lock.cjs +0 -81
- package/cjs/impl/sugar/watch.cjs +0 -159
- package/cjs/impl/trackedEmitter.cjs +0 -54
- package/cjs/impl/transactionErrors.cjs +0 -70
- package/cjs/impl/util.cjs +0 -64
- package/cjs/index.cjs +0 -132
- package/cjs/integration/changes.cjs +0 -76
- package/cjs/integration/disconnect-watch.cjs +0 -52
- package/cjs/integration/watch.cjs +0 -59
- package/cjs/schema/bind.cjs +0 -59
- package/cjs/schema/bulk.cjs +0 -92
- package/cjs/schema/changes.cjs +0 -68
- package/cjs/schema/config.cjs +0 -48
- package/cjs/schema/crud.cjs +0 -77
- package/cjs/schema/patch.cjs +0 -53
- package/cjs/schema/query.cjs +0 -62
- package/cjs/schema/stream.cjs +0 -42
- package/cjs/schema/sugar/lock.cjs +0 -59
- package/cjs/schema/sugar/watch.cjs +0 -42
- package/cjs/schema/util.cjs +0 -39
- package/config.json +0 -5
- package/docs/compiler.png +0 -0
- package/dualmode.config.json +0 -11
- package/impl/bulk.d.mts +0 -11
- package/impl/bulk.d.mts.map +0 -1
- package/impl/bulk.mjs +0 -291
- package/impl/changes.d.mts +0 -12
- package/impl/changes.d.mts.map +0 -1
- package/impl/changes.mjs +0 -53
- package/impl/crud.d.mts +0 -7
- package/impl/crud.d.mts.map +0 -1
- package/impl/crud.mjs +0 -108
- package/impl/errors.d.mts +0 -43
- package/impl/errors.d.mts.map +0 -1
- package/impl/errors.mjs +0 -65
- package/impl/logger.d.mts +0 -32
- package/impl/logger.d.mts.map +0 -1
- package/impl/logger.mjs +0 -59
- package/impl/patch.d.mts +0 -6
- package/impl/patch.d.mts.map +0 -1
- package/impl/patch.mjs +0 -88
- package/impl/query.d.mts +0 -195
- package/impl/query.d.mts.map +0 -1
- package/impl/query.mjs +0 -122
- package/impl/queryBuilder.d.mts +0 -154
- package/impl/queryBuilder.d.mts.map +0 -1
- package/impl/queryBuilder.mjs +0 -175
- package/impl/retry.d.mts +0 -2
- package/impl/retry.d.mts.map +0 -1
- package/impl/retry.mjs +0 -39
- package/impl/stream.d.mts +0 -3
- package/impl/stream.d.mts.map +0 -1
- package/impl/stream.mjs +0 -98
- package/impl/sugar/lock.d.mts +0 -5
- package/impl/sugar/lock.d.mts.map +0 -1
- package/impl/sugar/lock.mjs +0 -70
- package/impl/sugar/watch.d.mts +0 -34
- package/impl/sugar/watch.d.mts.map +0 -1
- package/impl/trackedEmitter.d.mts +0 -8
- package/impl/trackedEmitter.d.mts.map +0 -1
- package/impl/transactionErrors.d.mts.map +0 -1
- package/impl/transactionErrors.mjs +0 -47
- package/impl/util.d.mts +0 -3
- package/impl/util.d.mts.map +0 -1
- package/impl/util.mjs +0 -35
- package/index.d.mts +0 -80
- package/index.d.mts.map +0 -1
- package/index.mjs +0 -141
- package/integration/changes.mjs +0 -60
- package/integration/disconnect-watch.mjs +0 -36
- package/integration/watch.mjs +0 -40
- package/schema/bind.d.mts +0 -5461
- package/schema/bind.d.mts.map +0 -1
- package/schema/bind.mjs +0 -43
- package/schema/bulk.d.mts +0 -923
- package/schema/bulk.d.mts.map +0 -1
- package/schema/bulk.mjs +0 -83
- package/schema/changes.d.mts +0 -191
- package/schema/changes.d.mts.map +0 -1
- package/schema/changes.mjs +0 -59
- package/schema/config.d.mts +0 -79
- package/schema/config.d.mts.map +0 -1
- package/schema/config.mjs +0 -26
- package/schema/crud.d.mts +0 -491
- package/schema/crud.d.mts.map +0 -1
- package/schema/crud.mjs +0 -64
- package/schema/patch.d.mts +0 -255
- package/schema/patch.d.mts.map +0 -1
- package/schema/patch.mjs +0 -42
- package/schema/query.d.mts +0 -406
- package/schema/query.d.mts.map +0 -1
- package/schema/query.mjs +0 -45
- package/schema/stream.d.mts +0 -211
- package/schema/stream.d.mts.map +0 -1
- package/schema/stream.mjs +0 -23
- package/schema/sugar/lock.d.mts +0 -238
- package/schema/sugar/lock.d.mts.map +0 -1
- package/schema/sugar/lock.mjs +0 -50
- package/schema/sugar/watch.d.mts +0 -127
- package/schema/sugar/watch.d.mts.map +0 -1
- package/schema/sugar/watch.mjs +0 -29
- package/schema/util.d.mts +0 -160
- package/schema/util.d.mts.map +0 -1
- package/schema/util.mjs +0 -35
- 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.
|
|
4
|
-
import { createLogger } from '../logger.
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
-
|
|
98
|
+
// @ts-expect-error bad type?
|
|
99
|
+
currentRequest?.destroy()
|
|
68
100
|
handleReconnect()
|
|
69
101
|
} else {
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
+
}
|