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.
- package/.prettierrc +7 -0
- package/README.md +270 -218
- package/dist/cjs/index.cjs +1952 -0
- package/dist/esm/index.mjs +1898 -0
- package/docs/.nojekyll +1 -0
- package/docs/assets/hierarchy.js +1 -0
- package/docs/assets/highlight.css +113 -0
- package/docs/assets/icons.js +18 -0
- package/docs/assets/icons.svg +1 -0
- package/docs/assets/main.js +60 -0
- package/docs/assets/navigation.js +1 -0
- package/docs/assets/search.js +1 -0
- package/docs/assets/style.css +1633 -0
- package/docs/classes/QueryBuilder.html +42 -0
- package/docs/functions/bindConfig.html +4 -0
- package/docs/functions/bulkGet.html +14 -0
- package/docs/functions/bulkGetDictionary.html +10 -0
- package/docs/functions/bulkRemove.html +12 -0
- package/docs/functions/bulkRemoveMap.html +11 -0
- package/docs/functions/bulkSave.html +10 -0
- package/docs/functions/bulkSaveTransaction.html +23 -0
- package/docs/functions/createLock.html +7 -0
- package/docs/functions/createQuery.html +1 -0
- package/docs/functions/get.html +1 -0
- package/docs/functions/getAtRev.html +1 -0
- package/docs/functions/getDBInfo.html +10 -0
- package/docs/functions/patch.html +8 -0
- package/docs/functions/patchDangerously.html +9 -0
- package/docs/functions/put.html +1 -0
- package/docs/functions/query.html +15 -0
- package/docs/functions/queryStream.html +6 -0
- package/docs/functions/remove.html +1 -0
- package/docs/functions/removeLock.html +6 -0
- package/docs/functions/watchDocs.html +9 -0
- package/docs/functions/withRetry.html +6 -0
- package/docs/hierarchy.html +1 -0
- package/docs/index.html +483 -0
- package/docs/interfaces/NetworkError.html +6 -0
- package/docs/interfaces/NotFoundError.html +10 -0
- package/docs/interfaces/RetryOptions.html +10 -0
- package/docs/interfaces/RetryableError.html +10 -0
- package/docs/interfaces/StandardSchemaV1.FailureResult.html +4 -0
- package/docs/interfaces/StandardSchemaV1.Issue.html +6 -0
- package/docs/interfaces/StandardSchemaV1.Options.html +3 -0
- package/docs/interfaces/StandardSchemaV1.PathSegment.html +4 -0
- package/docs/interfaces/StandardSchemaV1.Props.html +10 -0
- package/docs/interfaces/StandardSchemaV1.SuccessResult.html +6 -0
- package/docs/interfaces/StandardSchemaV1.Types.html +6 -0
- package/docs/interfaces/StandardSchemaV1.html +4 -0
- package/docs/modules/StandardSchemaV1.html +1 -0
- package/docs/modules.html +1 -0
- package/docs/types/BoundInstance.html +1 -0
- package/docs/types/BulkGetBound.html +2 -0
- package/docs/types/BulkGetDictionaryBound.html +1 -0
- package/docs/types/BulkGetDictionaryOptions.html +2 -0
- package/docs/types/BulkGetDictionaryResult.html +3 -0
- package/docs/types/BulkGetOptions.html +3 -0
- package/docs/types/BulkGetResponse.html +1 -0
- package/docs/types/CouchConfig-1.html +1 -0
- package/docs/types/CouchConfig.html +1 -0
- package/docs/types/CouchConfigInput.html +1 -0
- package/docs/types/CouchDoc-1.html +1 -0
- package/docs/types/CouchDoc.html +2 -0
- package/docs/types/CouchDocInput.html +2 -0
- package/docs/types/GetAtRevBound.html +1 -0
- package/docs/types/GetBound.html +1 -0
- package/docs/types/GetOptions.html +2 -0
- package/docs/types/LockDoc-1.html +1 -0
- package/docs/types/LockDoc.html +1 -0
- package/docs/types/LockOptions-1.html +1 -0
- package/docs/types/LockOptions.html +1 -0
- package/docs/types/LockOptionsInput.html +1 -0
- package/docs/types/OnInvalidDocAction.html +1 -0
- package/docs/types/OnRow.html +1 -0
- package/docs/types/QueryBound.html +1 -0
- package/docs/types/SimpleViewOptions-1.html +1 -0
- package/docs/types/SimpleViewOptions.html +1 -0
- package/docs/types/StandardSchemaV1.InferInput.html +2 -0
- package/docs/types/StandardSchemaV1.InferOutput.html +2 -0
- package/docs/types/StandardSchemaV1.Result.html +2 -0
- package/docs/types/ViewQueryResponse-1.html +1 -0
- package/docs/types/ViewQueryResponse.html +2 -0
- package/docs/types/ViewQueryResponseValidated.html +2 -0
- package/docs/types/ViewRow-1.html +1 -0
- package/docs/types/ViewRow.html +2 -0
- package/docs/types/ViewRowValidated.html +7 -0
- package/docs/types/ViewString.html +1 -0
- package/docs/types/WatchOptionsInput.html +1 -0
- package/docs/types/WatchOptionsSchema-1.html +1 -0
- package/docs/types/WatchOptionsSchema.html +1 -0
- package/eslint.config.js +15 -0
- package/impl/bindConfig.mts +140 -0
- package/impl/bulkGet.mts +256 -0
- package/impl/bulkGet.test.mts +159 -0
- package/impl/bulkRemove.mts +98 -0
- package/impl/bulkRemove.test.mts +102 -0
- package/impl/bulkSave.mts +286 -0
- package/impl/bulkSave.test.mts +319 -0
- package/impl/get.mts +137 -0
- package/impl/get.test.mts +114 -0
- package/impl/getDBInfo.mts +67 -0
- package/impl/getDBInfo.test.mts +62 -0
- package/impl/patch.mts +134 -0
- package/impl/patch.test.mts +142 -0
- package/impl/put.mts +56 -0
- package/impl/put.test.mts +114 -0
- package/impl/query.mts +224 -0
- package/impl/query.test.mts +280 -0
- package/impl/remove.mts +65 -0
- package/impl/remove.test.mts +82 -0
- package/impl/retry.mts +66 -0
- package/impl/retry.test.mts +77 -0
- package/impl/stream.mts +143 -0
- package/impl/stream.test.mts +205 -0
- package/impl/sugar/lock.mts +103 -0
- package/impl/sugar/lock.test.mts +113 -0
- package/impl/sugar/{watch.mjs → watch.mts} +56 -22
- package/impl/sugar/watch.test.mts +155 -0
- package/impl/utils/errors.mts +130 -0
- package/impl/utils/errors.test.mts +58 -0
- package/impl/utils/logger.mts +62 -0
- package/impl/utils/logger.test.mts +129 -0
- package/impl/utils/mergeNeedleOpts.mts +16 -0
- package/impl/utils/parseRows.mts +117 -0
- package/impl/utils/parseRows.test.mts +183 -0
- package/impl/utils/queryBuilder.mts +173 -0
- package/impl/utils/queryBuilder.test.mts +83 -0
- package/impl/utils/queryString.mts +44 -0
- package/impl/utils/queryString.test.mts +53 -0
- package/impl/{trackedEmitter.mjs → utils/trackedEmitter.mts} +9 -7
- package/impl/utils/transactionErrors.mts +71 -0
- package/index.mts +82 -0
- package/index.test.mts +415 -0
- package/package.json +45 -31
- 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/log.txt +0 -580
- 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
package/impl/stream.mts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import needle from 'needle'
|
|
2
|
+
import type { IncomingMessage } from 'node:http'
|
|
3
|
+
import Chain from 'stream-chain'
|
|
4
|
+
import Parser from 'stream-json/Parser.js'
|
|
5
|
+
import Pick from 'stream-json/filters/Pick.js'
|
|
6
|
+
import StreamArray from 'stream-json/streamers/StreamArray.js'
|
|
7
|
+
import { CouchConfig, type CouchConfigInput } from '../schema/config.mts'
|
|
8
|
+
import { RetryableError } from './utils/errors.mts'
|
|
9
|
+
import { createLogger } from './utils/logger.mts'
|
|
10
|
+
import { queryString } from './utils/queryString.mts'
|
|
11
|
+
import { mergeNeedleOpts } from './utils/mergeNeedleOpts.mts'
|
|
12
|
+
import type { ViewRow } from '../schema/couch/couch.output.schema.ts'
|
|
13
|
+
import type { ViewOptions } from '../schema/couch/couch.input.schema.ts'
|
|
14
|
+
|
|
15
|
+
type StreamArrayChunk<Row> = {
|
|
16
|
+
key: number
|
|
17
|
+
value: Row
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type OnRow = (row: ViewRow) => void
|
|
21
|
+
type HttpMethod = 'GET' | 'POST'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute a CouchDB view query and stream rows as they are received.
|
|
25
|
+
* @param rawConfig CouchDB configuration
|
|
26
|
+
* @param view The CouchDB view to query
|
|
27
|
+
* @param options Query options
|
|
28
|
+
* @param onRow Callback invoked for each row received
|
|
29
|
+
*/
|
|
30
|
+
export async function queryStream(
|
|
31
|
+
rawConfig: CouchConfigInput,
|
|
32
|
+
view: string,
|
|
33
|
+
options: ViewOptions | undefined,
|
|
34
|
+
onRow: OnRow
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const config = CouchConfig.parse(rawConfig)
|
|
38
|
+
const logger = createLogger(config)
|
|
39
|
+
logger.info(`Starting view query stream: ${view}`)
|
|
40
|
+
logger.debug('Query options:', options)
|
|
41
|
+
|
|
42
|
+
const queryOptions: ViewOptions = options ?? {}
|
|
43
|
+
|
|
44
|
+
let method: HttpMethod = 'GET'
|
|
45
|
+
let payload: Record<string, unknown> | null = null
|
|
46
|
+
let qs = queryString(queryOptions)
|
|
47
|
+
logger.debug('Generated query string:', qs)
|
|
48
|
+
|
|
49
|
+
if (typeof queryOptions.keys !== 'undefined') {
|
|
50
|
+
const MAX_URL_LENGTH = 2000
|
|
51
|
+
const keysAsString = `keys=${encodeURIComponent(JSON.stringify(queryOptions.keys))}`
|
|
52
|
+
|
|
53
|
+
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
|
|
54
|
+
qs += (qs.length > 0 ? '&' : '') + keysAsString
|
|
55
|
+
} else {
|
|
56
|
+
method = 'POST'
|
|
57
|
+
payload = { keys: queryOptions.keys }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const url = `${config.couch}/${view}?${qs}`
|
|
62
|
+
const opts = {
|
|
63
|
+
json: true,
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
},
|
|
67
|
+
parse_response: false as const
|
|
68
|
+
}
|
|
69
|
+
const mergedOpts = mergeNeedleOpts(config, opts)
|
|
70
|
+
|
|
71
|
+
const parserPipeline = Chain.chain([
|
|
72
|
+
new Parser(),
|
|
73
|
+
new Pick({ filter: 'rows' }),
|
|
74
|
+
new StreamArray()
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
let rowCount = 0
|
|
78
|
+
let settled = false
|
|
79
|
+
|
|
80
|
+
const settleReject = (err: Error) => {
|
|
81
|
+
if (settled) return
|
|
82
|
+
settled = true
|
|
83
|
+
reject(err)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const settleResolve = () => {
|
|
87
|
+
if (settled) return
|
|
88
|
+
settled = true
|
|
89
|
+
resolve()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let request: ReturnType<typeof needle.get> | ReturnType<typeof needle.post> | null = null
|
|
93
|
+
|
|
94
|
+
parserPipeline.on('data', (chunk: StreamArrayChunk<ViewRow>) => {
|
|
95
|
+
try {
|
|
96
|
+
rowCount++
|
|
97
|
+
onRow(chunk.value)
|
|
98
|
+
} catch (callbackErr) {
|
|
99
|
+
const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr))
|
|
100
|
+
parserPipeline.destroy(error)
|
|
101
|
+
settleReject(error)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
parserPipeline.on('error', (err: Error) => {
|
|
106
|
+
logger.error('Stream parsing error:', err)
|
|
107
|
+
parserPipeline.destroy()
|
|
108
|
+
settleReject(new Error(`Stream parsing error: ${err.message}`, { cause: err }))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
parserPipeline.on('end', () => {
|
|
112
|
+
logger.info(`Stream completed, processed ${rowCount} rows`)
|
|
113
|
+
settleResolve()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
request = method === 'GET' ? needle.get(url, mergedOpts) : needle.post(url, payload, mergedOpts)
|
|
117
|
+
|
|
118
|
+
request.on('response', (response: IncomingMessage) => {
|
|
119
|
+
logger.debug(`Received response with status code: ${response.statusCode}`)
|
|
120
|
+
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
121
|
+
logger.warn(`Retryable status code received: ${response.statusCode}`)
|
|
122
|
+
settleReject(new RetryableError('retryable error during stream query', response.statusCode))
|
|
123
|
+
// @ts-expect-error bad type?
|
|
124
|
+
request.destroy()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
request.on('error', (err: NodeJS.ErrnoException) => {
|
|
129
|
+
logger.error('Network error during stream query:', err)
|
|
130
|
+
parserPipeline.destroy(err)
|
|
131
|
+
try {
|
|
132
|
+
RetryableError.handleNetworkError(err)
|
|
133
|
+
} catch (retryErr) {
|
|
134
|
+
settleReject(retryErr as Error)
|
|
135
|
+
return
|
|
136
|
+
} finally {
|
|
137
|
+
settleReject(err)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
request.pipe(parserPipeline)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { suite } from 'node:test'
|
|
3
|
+
import { createServer } from 'node:http'
|
|
4
|
+
import type { AddressInfo } from 'node:net'
|
|
5
|
+
import { queryStream } from './stream.mts'
|
|
6
|
+
import { bindConfig } from './bindConfig.mts'
|
|
7
|
+
|
|
8
|
+
const startServer = async (handler: Parameters<typeof createServer>[0]) => {
|
|
9
|
+
const server = createServer(handler)
|
|
10
|
+
await new Promise<void>(resolve => server.listen(0, resolve))
|
|
11
|
+
return server
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
suite('queryStream', () => {
|
|
15
|
+
test('queryStream streams rows from chunked response', async t => {
|
|
16
|
+
const expectedRows = [
|
|
17
|
+
{ id: 'row-1', key: 'row-1', value: { count: 1 } },
|
|
18
|
+
{ id: 'row-2', key: 'row-2', value: { count: 2 } }
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
// @ts-expect-error testing server
|
|
22
|
+
const server = await startServer((req, res) => {
|
|
23
|
+
res.on('error', () => {})
|
|
24
|
+
const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
|
|
25
|
+
assert.strictEqual(req.method, 'GET')
|
|
26
|
+
assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
|
|
27
|
+
|
|
28
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
29
|
+
const payload = JSON.stringify({
|
|
30
|
+
total_rows: expectedRows.length,
|
|
31
|
+
rows: expectedRows
|
|
32
|
+
})
|
|
33
|
+
const chunkSize = 7
|
|
34
|
+
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
35
|
+
res.write(payload.slice(i, i + chunkSize))
|
|
36
|
+
}
|
|
37
|
+
res.end()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
t.after(async () => {
|
|
41
|
+
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const { port } = server.address() as AddressInfo
|
|
45
|
+
const rows: unknown[] = []
|
|
46
|
+
|
|
47
|
+
await queryStream(
|
|
48
|
+
{ couch: `http://127.0.0.1:${port}` },
|
|
49
|
+
'_design/demo/_view/by-key',
|
|
50
|
+
{},
|
|
51
|
+
row => {
|
|
52
|
+
const matchedRow = expectedRows.find(r => r.id === row.id)
|
|
53
|
+
assert.ok(matchedRow)
|
|
54
|
+
assert.deepStrictEqual(row, matchedRow)
|
|
55
|
+
rows.push(row)
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert.deepStrictEqual(rows, expectedRows)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('queryStream works with bindConfig', async t => {
|
|
63
|
+
const expectedRows = [
|
|
64
|
+
{ id: 'row-1', key: 'row-1', value: { count: 1 } },
|
|
65
|
+
{ id: 'row-2', key: 'row-2', value: { count: 2 } }
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
// @ts-expect-error testing server
|
|
69
|
+
const server = await startServer((req, res) => {
|
|
70
|
+
res.on('error', () => {})
|
|
71
|
+
const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
|
|
72
|
+
assert.strictEqual(req.method, 'GET')
|
|
73
|
+
assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
|
|
74
|
+
|
|
75
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
76
|
+
const payload = JSON.stringify({
|
|
77
|
+
total_rows: expectedRows.length,
|
|
78
|
+
rows: expectedRows
|
|
79
|
+
})
|
|
80
|
+
const chunkSize = 7
|
|
81
|
+
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
82
|
+
res.write(payload.slice(i, i + chunkSize))
|
|
83
|
+
}
|
|
84
|
+
res.end()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
t.after(async () => {
|
|
88
|
+
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const { port } = server.address() as AddressInfo
|
|
92
|
+
|
|
93
|
+
const rows: unknown[] = []
|
|
94
|
+
|
|
95
|
+
const db = bindConfig({ couch: `http://127.0.0.1:${port}` })
|
|
96
|
+
|
|
97
|
+
await db.queryStream('_design/demo/_view/by-key', {}, row => {
|
|
98
|
+
const matchedRow = expectedRows.find(r => r.id === row.id)
|
|
99
|
+
assert.ok(matchedRow)
|
|
100
|
+
assert.deepStrictEqual(row, matchedRow)
|
|
101
|
+
rows.push(row)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
assert.deepStrictEqual(rows, expectedRows)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('queryStream works with options chaining', async t => {
|
|
108
|
+
const expectedRows = [
|
|
109
|
+
{ id: 'row-1', key: 'row-1', value: { count: 1 } },
|
|
110
|
+
{ id: 'row-2', key: 'row-2', value: { count: 2 } }
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
// @ts-expect-error testing server
|
|
114
|
+
const server = await startServer((req, res) => {
|
|
115
|
+
res.on('error', () => {})
|
|
116
|
+
const requestUrl = new URL(req.url ?? '/', `http://${req.headers.host}`)
|
|
117
|
+
assert.strictEqual(req.method, 'GET')
|
|
118
|
+
assert.strictEqual(requestUrl.pathname, '/_design/demo/_view/by-key')
|
|
119
|
+
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
121
|
+
const payload = JSON.stringify({
|
|
122
|
+
total_rows: expectedRows.length,
|
|
123
|
+
rows: expectedRows
|
|
124
|
+
})
|
|
125
|
+
const chunkSize = 7
|
|
126
|
+
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
127
|
+
res.write(payload.slice(i, i + chunkSize))
|
|
128
|
+
}
|
|
129
|
+
res.end()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
t.after(async () => {
|
|
133
|
+
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const { port } = server.address() as AddressInfo
|
|
137
|
+
const rows: unknown[] = []
|
|
138
|
+
|
|
139
|
+
const db = bindConfig({ couch: `http://127.0.0.1:${port}` })
|
|
140
|
+
|
|
141
|
+
await db.options({ logger: console }).queryStream('_design/demo/_view/by-key', {}, row => {
|
|
142
|
+
const matchedRow = expectedRows.find(r => r.id === row.id)
|
|
143
|
+
assert.ok(matchedRow)
|
|
144
|
+
assert.deepStrictEqual(row, matchedRow)
|
|
145
|
+
rows.push(row)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
assert.deepStrictEqual(rows, expectedRows)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('queryStream handles empty result sets', async t => {
|
|
152
|
+
// @ts-expect-error testing server
|
|
153
|
+
const server = await startServer((_, res) => {
|
|
154
|
+
res.on('error', () => {})
|
|
155
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
156
|
+
res.write('{"rows":[]}')
|
|
157
|
+
res.end()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
t.after(async () => {
|
|
161
|
+
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const { port } = server.address() as AddressInfo
|
|
165
|
+
let rowCount = 0
|
|
166
|
+
|
|
167
|
+
await queryStream(
|
|
168
|
+
{ couch: `http://127.0.0.1:${port}` },
|
|
169
|
+
'_design/demo/_view/by-key',
|
|
170
|
+
{},
|
|
171
|
+
() => {
|
|
172
|
+
rowCount++
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(rowCount, 0)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('queryStream rejects when row handler throws', async t => {
|
|
180
|
+
// @ts-expect-error testing server
|
|
181
|
+
const server = await startServer((_, res) => {
|
|
182
|
+
res.on('error', () => {})
|
|
183
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
184
|
+
res.write('{"rows":[{"id":"broken","value":42}]}')
|
|
185
|
+
res.end()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
t.after(async () => {
|
|
189
|
+
await new Promise<void>(resolve => server.close(() => resolve()))
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const { port } = server.address() as AddressInfo
|
|
193
|
+
const handlerError = new Error('row-failure')
|
|
194
|
+
|
|
195
|
+
await assert.rejects(
|
|
196
|
+
queryStream({ couch: `http://127.0.0.1:${port}` }, '_design/demo/_view/error', {}, () => {
|
|
197
|
+
throw handlerError
|
|
198
|
+
}),
|
|
199
|
+
error => {
|
|
200
|
+
assert.strictEqual(error, handlerError)
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { LockOptions } from '../../schema/sugar/lock.mts'
|
|
2
|
+
import { put } from '../put.mts'
|
|
3
|
+
import { get } from '../get.mts'
|
|
4
|
+
import { createLogger } from '../utils/logger.mts'
|
|
5
|
+
import { CouchConfig, type CouchConfigInput } from '../../schema/config.mts'
|
|
6
|
+
import { isConflictError } from '../utils/errors.mts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a lock document for the specified document ID.
|
|
10
|
+
* Returns true if the lock was created, false if locking is disabled or a conflict occurred.
|
|
11
|
+
*
|
|
12
|
+
* @param configInput CouchDB configuration
|
|
13
|
+
* @param docId The document ID to lock
|
|
14
|
+
* @param lockOptions Locking options
|
|
15
|
+
*
|
|
16
|
+
* @return True if the lock was created, false otherwise
|
|
17
|
+
*/
|
|
18
|
+
export async function createLock(
|
|
19
|
+
configInput: CouchConfigInput,
|
|
20
|
+
docId: string,
|
|
21
|
+
lockOptions: LockOptions
|
|
22
|
+
): Promise<boolean> {
|
|
23
|
+
const config = CouchConfig.parse(configInput)
|
|
24
|
+
const options = LockOptions.parse(lockOptions)
|
|
25
|
+
|
|
26
|
+
const logger = createLogger(config)
|
|
27
|
+
|
|
28
|
+
if (!options.enableLocking) {
|
|
29
|
+
logger.debug('Locking disabled, returning true without creating lock')
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const _id = `lock-${docId}`
|
|
34
|
+
const lock = {
|
|
35
|
+
_id,
|
|
36
|
+
type: 'lock',
|
|
37
|
+
locks: docId,
|
|
38
|
+
lockedAt: new Date().toISOString(),
|
|
39
|
+
lockedBy: options.username
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await put(config, lock)
|
|
44
|
+
logger.info(`Lock created for ${docId} by ${options.username}`)
|
|
45
|
+
return result.ok === true
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (isConflictError(error)) {
|
|
48
|
+
logger.warn(`Lock conflict for ${docId} - already locked`)
|
|
49
|
+
} else {
|
|
50
|
+
logger.error(`Error creating lock for ${docId}:`, error)
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Remove the lock document for the specified document ID if owned by the caller.
|
|
58
|
+
*
|
|
59
|
+
* @param configInput CouchDB configuration
|
|
60
|
+
* @param docId The document ID to unlock
|
|
61
|
+
* @param lockOptions Locking options
|
|
62
|
+
*
|
|
63
|
+
* @return Promise that resolves when the unlock operation is complete
|
|
64
|
+
*/
|
|
65
|
+
export async function removeLock(
|
|
66
|
+
configInput: CouchConfigInput,
|
|
67
|
+
docId: string,
|
|
68
|
+
lockOptions: LockOptions
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const config = CouchConfig.parse(configInput)
|
|
71
|
+
const options = LockOptions.parse(lockOptions)
|
|
72
|
+
const logger = createLogger(config)
|
|
73
|
+
|
|
74
|
+
if (!options.enableLocking) {
|
|
75
|
+
logger.debug('Locking disabled, skipping unlock')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!docId) {
|
|
80
|
+
logger.warn('No docId provided for unlock')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const _id = `lock-${docId}`
|
|
85
|
+
const existingLock = await get(config, _id)
|
|
86
|
+
|
|
87
|
+
if (!existingLock) {
|
|
88
|
+
logger.debug(`No lock found for ${docId}`)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (existingLock.lockedBy !== options.username) {
|
|
93
|
+
logger.warn(`Cannot remove lock for ${docId} - owned by ${existingLock.lockedBy}`)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await put(config, { ...existingLock, _deleted: true })
|
|
99
|
+
logger.info(`Lock removed for ${docId}`)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.error(`Error removing lock for ${docId}:`, error)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 { createLock, removeLock } from './lock.mts'
|
|
6
|
+
import { TEST_DB_URL } from '../../test/setup-db.mts'
|
|
7
|
+
|
|
8
|
+
const baseConfig: CouchConfigInput = {
|
|
9
|
+
couch: TEST_DB_URL,
|
|
10
|
+
useConsoleLogger: true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function fetchLockDoc(docId: string) {
|
|
14
|
+
return await needle('get', `${TEST_DB_URL}/lock-${docId}`, null, {
|
|
15
|
+
json: true
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
suite('lock', () => {
|
|
20
|
+
test('integration with pouchdb-server', async t => {
|
|
21
|
+
await t.test('creates a lock document when enabled', async () => {
|
|
22
|
+
const docId = `lock-creates-${Date.now()}`
|
|
23
|
+
const created = await createLock(baseConfig, docId, {
|
|
24
|
+
enableLocking: true,
|
|
25
|
+
username: 'alice'
|
|
26
|
+
})
|
|
27
|
+
assert.strictEqual(created, true)
|
|
28
|
+
|
|
29
|
+
const response = await fetchLockDoc(docId)
|
|
30
|
+
assert.strictEqual(response.statusCode, 200)
|
|
31
|
+
const body = response.body as { lockedBy: string; locks: string }
|
|
32
|
+
assert.strictEqual(body.lockedBy, 'alice')
|
|
33
|
+
assert.strictEqual(body.locks, docId)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await t.test('returns true without writing when locking disabled', async () => {
|
|
37
|
+
const docId = `lock-disabled-${Date.now()}`
|
|
38
|
+
const created = await createLock(baseConfig, docId, {
|
|
39
|
+
enableLocking: false,
|
|
40
|
+
username: 'anyone'
|
|
41
|
+
})
|
|
42
|
+
assert.strictEqual(created, true)
|
|
43
|
+
|
|
44
|
+
const response = await fetchLockDoc(docId)
|
|
45
|
+
assert.strictEqual(response.statusCode, 404)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await t.test('returns false on conflict and keeps existing lock', async () => {
|
|
49
|
+
const docId = `lock-conflict-${Date.now()}`
|
|
50
|
+
await createLock(baseConfig, docId, {
|
|
51
|
+
enableLocking: true,
|
|
52
|
+
username: 'alice'
|
|
53
|
+
})
|
|
54
|
+
const created = await createLock(baseConfig, docId, {
|
|
55
|
+
enableLocking: true,
|
|
56
|
+
username: 'bob'
|
|
57
|
+
})
|
|
58
|
+
assert.strictEqual(created, false)
|
|
59
|
+
|
|
60
|
+
const response = await fetchLockDoc(docId)
|
|
61
|
+
assert.strictEqual(response.statusCode, 200)
|
|
62
|
+
const body = response.body as { lockedBy: string }
|
|
63
|
+
assert.strictEqual(body.lockedBy, 'alice')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
await t.test('removes lock when owned by caller', async () => {
|
|
67
|
+
const docId = `lock-remove-${Date.now()}`
|
|
68
|
+
await createLock(baseConfig, docId, {
|
|
69
|
+
enableLocking: true,
|
|
70
|
+
username: 'alice'
|
|
71
|
+
})
|
|
72
|
+
await removeLock(baseConfig, docId, {
|
|
73
|
+
enableLocking: true,
|
|
74
|
+
username: 'alice'
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const response = await fetchLockDoc(docId)
|
|
78
|
+
assert.strictEqual(response.statusCode, 404)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await t.test('skips removal when lock owned by someone else', async () => {
|
|
82
|
+
const docId = `lock-remove-others-${Date.now()}`
|
|
83
|
+
await createLock(baseConfig, docId, {
|
|
84
|
+
enableLocking: true,
|
|
85
|
+
username: 'alice'
|
|
86
|
+
})
|
|
87
|
+
await removeLock(baseConfig, docId, {
|
|
88
|
+
enableLocking: true,
|
|
89
|
+
username: 'bob'
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const response = await fetchLockDoc(docId)
|
|
93
|
+
assert.strictEqual(response.statusCode, 200)
|
|
94
|
+
const body = response.body as { lockedBy: string }
|
|
95
|
+
assert.strictEqual(body.lockedBy, 'alice')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await t.test('respects disabled removal', async () => {
|
|
99
|
+
const docId = `lock-disabled-remove-${Date.now()}`
|
|
100
|
+
await createLock(baseConfig, docId, {
|
|
101
|
+
enableLocking: true,
|
|
102
|
+
username: 'alice'
|
|
103
|
+
})
|
|
104
|
+
await removeLock(baseConfig, docId, {
|
|
105
|
+
enableLocking: false,
|
|
106
|
+
username: 'alice'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const response = await fetchLockDoc(docId)
|
|
110
|
+
assert.strictEqual(response.statusCode, 200)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
@@ -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
|
+
}
|