hide-a-bed 6.0.0 → 7.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/README.md +89 -28
- package/dist/cjs/index.cjs +888 -443
- package/dist/esm/index.mjs +883 -443
- package/eslint.config.js +6 -1
- package/impl/bindConfig.mts +30 -3
- package/impl/bulkGet.mts +50 -27
- package/impl/bulkRemove.mts +4 -2
- package/impl/bulkSave.mts +50 -28
- package/impl/get.mts +49 -40
- package/impl/getDBInfo.mts +26 -24
- package/impl/patch.mts +46 -42
- package/impl/put.mts +39 -21
- package/impl/query.mts +101 -81
- package/impl/remove.mts +33 -33
- package/impl/stream.mts +163 -102
- package/impl/sugar/watch.mts +165 -97
- package/impl/utils/errors.mts +261 -35
- package/impl/utils/fetch.mts +201 -0
- package/impl/utils/parseRows.mts +47 -6
- package/impl/utils/request.mts +22 -0
- package/impl/utils/response.mts +50 -0
- package/impl/utils/transactionErrors.mts +14 -8
- package/impl/utils/url.mts +21 -0
- package/index.mts +19 -2
- package/migration_guides/v7.md +353 -0
- package/package.json +4 -4
- package/schema/config.mts +17 -34
- package/schema/request.mts +36 -0
- package/schema/sugar/watch.mts +1 -1
- package/tsconfig.json +9 -1
- package/types/output/impl/bindConfig.d.mts +31 -149
- package/types/output/impl/bindConfig.d.mts.map +1 -1
- package/types/output/impl/bindConfig.test.d.mts +2 -0
- package/types/output/impl/bindConfig.test.d.mts.map +1 -0
- package/types/output/impl/bulkGet.d.mts +5 -5
- package/types/output/impl/bulkGet.d.mts.map +1 -1
- package/types/output/impl/bulkRemove.d.mts +4 -2
- package/types/output/impl/bulkRemove.d.mts.map +1 -1
- package/types/output/impl/bulkSave.d.mts +2 -2
- package/types/output/impl/bulkSave.d.mts.map +1 -1
- package/types/output/impl/get.d.mts +2 -2
- package/types/output/impl/get.d.mts.map +1 -1
- package/types/output/impl/getDBInfo.d.mts +1 -1
- package/types/output/impl/getDBInfo.d.mts.map +1 -1
- package/types/output/impl/patch.d.mts +8 -3
- package/types/output/impl/patch.d.mts.map +1 -1
- package/types/output/impl/put.d.mts.map +1 -1
- package/types/output/impl/query.d.mts +8 -23
- package/types/output/impl/query.d.mts.map +1 -1
- package/types/output/impl/remove.d.mts.map +1 -1
- package/types/output/impl/request-controls.test.d.mts +2 -0
- package/types/output/impl/request-controls.test.d.mts.map +1 -0
- package/types/output/impl/stream.d.mts +1 -1
- package/types/output/impl/stream.d.mts.map +1 -1
- package/types/output/impl/sugar/watch.d.mts +7 -5
- package/types/output/impl/sugar/watch.d.mts.map +1 -1
- package/types/output/impl/utils/errors.d.mts +84 -26
- package/types/output/impl/utils/errors.d.mts.map +1 -1
- package/types/output/impl/utils/fetch.d.mts +27 -0
- package/types/output/impl/utils/fetch.d.mts.map +1 -0
- package/types/output/impl/utils/fetch.test.d.mts +2 -0
- package/types/output/impl/utils/fetch.test.d.mts.map +1 -0
- package/types/output/impl/utils/parseRows.d.mts +3 -0
- package/types/output/impl/utils/parseRows.d.mts.map +1 -1
- package/types/output/impl/utils/request.d.mts +6 -0
- package/types/output/impl/utils/request.d.mts.map +1 -0
- package/types/output/impl/utils/response.d.mts +7 -0
- package/types/output/impl/utils/response.d.mts.map +1 -0
- package/types/output/impl/utils/response.test.d.mts +2 -0
- package/types/output/impl/utils/response.test.d.mts.map +1 -0
- package/types/output/impl/utils/trackedEmitter.test.d.mts +2 -0
- package/types/output/impl/utils/trackedEmitter.test.d.mts.map +1 -0
- package/types/output/impl/utils/transactionErrors.d.mts +5 -4
- package/types/output/impl/utils/transactionErrors.d.mts.map +1 -1
- package/types/output/impl/utils/transactionErrors.test.d.mts +2 -0
- package/types/output/impl/utils/transactionErrors.test.d.mts.map +1 -0
- package/types/output/impl/utils/url.d.mts +4 -0
- package/types/output/impl/utils/url.d.mts.map +1 -0
- package/types/output/impl/utils/url.test.d.mts +2 -0
- package/types/output/impl/utils/url.test.d.mts.map +1 -0
- package/types/output/index.d.mts +5 -2
- package/types/output/index.d.mts.map +1 -1
- package/types/output/schema/config.d.mts +13 -69
- package/types/output/schema/config.d.mts.map +1 -1
- package/types/output/schema/config.test.d.mts +2 -0
- package/types/output/schema/config.test.d.mts.map +1 -0
- package/types/output/schema/request.d.mts +10 -0
- package/types/output/schema/request.d.mts.map +1 -0
- package/types/output/schema/sugar/lock.test.d.mts +2 -0
- package/types/output/schema/sugar/lock.test.d.mts.map +1 -0
- package/types/output/schema/sugar/watch.d.mts +1 -1
- package/types/output/schema/sugar/watch.d.mts.map +1 -1
- package/types/output/schema/sugar/watch.test.d.mts +2 -0
- package/types/output/schema/sugar/watch.test.d.mts.map +1 -0
- package/impl/utils/mergeNeedleOpts.mts +0 -16
- package/schema/util.mts +0 -8
- package/types/output/impl/utils/mergeNeedleOpts.d.mts +0 -53
- package/types/output/impl/utils/mergeNeedleOpts.d.mts.map +0 -1
- package/types/output/schema/util.d.mts +0 -85
- package/types/output/schema/util.d.mts.map +0 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -28,8 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
let zod = require("zod");
|
|
29
29
|
zod = __toESM(zod);
|
|
30
30
|
let node_timers_promises = require("node:timers/promises");
|
|
31
|
-
let
|
|
32
|
-
needle = __toESM(needle);
|
|
31
|
+
let node_stream = require("node:stream");
|
|
33
32
|
let stream_chain = require("stream-chain");
|
|
34
33
|
stream_chain = __toESM(stream_chain);
|
|
35
34
|
let stream_json_Parser_js = require("stream-json/Parser.js");
|
|
@@ -182,6 +181,21 @@ var QueryBuilder = class {
|
|
|
182
181
|
};
|
|
183
182
|
const createQuery = () => new QueryBuilder();
|
|
184
183
|
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region schema/request.mts
|
|
186
|
+
const isAbortSignal = (value) => {
|
|
187
|
+
return value instanceof AbortSignal;
|
|
188
|
+
};
|
|
189
|
+
const isDispatcher = (value) => {
|
|
190
|
+
if (typeof value !== "object" || value === null) return false;
|
|
191
|
+
return typeof value.dispatch === "function";
|
|
192
|
+
};
|
|
193
|
+
const RequestOptions = zod.z.strictObject({
|
|
194
|
+
dispatcher: zod.z.custom(isDispatcher, { message: "dispatcher must expose a dispatch method" }).optional().describe("dispatcher to use for the request"),
|
|
195
|
+
signal: zod.z.custom(isAbortSignal, { message: "signal must be an AbortSignal" }).optional().describe("abort signal for the request"),
|
|
196
|
+
timeout: zod.z.number().nonnegative().optional().describe("request timeout in milliseconds")
|
|
197
|
+
});
|
|
198
|
+
|
|
185
199
|
//#endregion
|
|
186
200
|
//#region schema/config.mts
|
|
187
201
|
const anyArgs = zod.z.array(zod.z.any());
|
|
@@ -206,56 +220,61 @@ const LoggerSchema = zod.z.object({
|
|
|
206
220
|
input: anyArgs,
|
|
207
221
|
output: zod.z.void()
|
|
208
222
|
}));
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
parse_response: zod.z.boolean().optional()
|
|
223
|
+
const CouchAuth = zod.z.strictObject({
|
|
224
|
+
username: zod.z.string().describe("basic auth username for CouchDB requests"),
|
|
225
|
+
password: zod.z.string().describe("basic auth password for CouchDB requests")
|
|
213
226
|
});
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
timeout: zod.z.number().optional(),
|
|
222
|
-
read_timeout: zod.z.number().optional(),
|
|
223
|
-
parse_response: zod.z.boolean().optional(),
|
|
224
|
-
decode: zod.z.boolean().optional(),
|
|
225
|
-
parse_cookies: zod.z.boolean().optional(),
|
|
226
|
-
cookies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
227
|
-
headers: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
228
|
-
auth: zod.z.enum([
|
|
229
|
-
"auto",
|
|
230
|
-
"digest",
|
|
231
|
-
"basic"
|
|
232
|
-
]).optional(),
|
|
233
|
-
username: zod.z.string().optional(),
|
|
234
|
-
password: zod.z.string().optional(),
|
|
235
|
-
proxy: zod.z.string().optional(),
|
|
236
|
-
agent: zod.z.any().optional(),
|
|
237
|
-
rejectUnauthorized: zod.z.boolean().optional(),
|
|
238
|
-
output: zod.z.string().optional(),
|
|
239
|
-
parse: zod.z.boolean().optional(),
|
|
240
|
-
multipart: zod.z.boolean().optional(),
|
|
241
|
-
open_timeout: zod.z.number().optional(),
|
|
242
|
-
response_timeout: zod.z.number().optional(),
|
|
243
|
-
keepAlive: zod.z.boolean().optional()
|
|
227
|
+
const CouchUrl = zod.z.custom((value) => {
|
|
228
|
+
try {
|
|
229
|
+
const url = new URL(value);
|
|
230
|
+
return url.username === "" && url.password === "";
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
244
234
|
});
|
|
245
235
|
const CouchConfig = zod.z.strictObject({
|
|
236
|
+
auth: CouchAuth.optional().describe("basic auth credentials for CouchDB requests"),
|
|
246
237
|
backoffFactor: zod.z.number().optional().default(2).describe("multiplier for exponential backoff"),
|
|
247
238
|
bindWithRetry: zod.z.boolean().optional().default(true).describe("should we bind with retry"),
|
|
248
|
-
couch:
|
|
239
|
+
couch: CouchUrl.describe("URL of the couch db without embedded credentials"),
|
|
249
240
|
initialDelay: zod.z.number().optional().default(1e3).describe("initial retry delay in milliseconds"),
|
|
250
241
|
logger: LoggerSchema.optional().describe("logging interface supporting winston-like or simple function interface"),
|
|
251
242
|
maxRetries: zod.z.number().optional().default(3).describe("maximum number of retry attempts"),
|
|
252
|
-
|
|
253
|
-
throwOnGetNotFound: zod.z.boolean().optional().default(false).describe("if
|
|
243
|
+
request: RequestOptions.optional().describe("default request controls for CouchDB requests"),
|
|
244
|
+
throwOnGetNotFound: zod.z.boolean().optional().default(false).describe("if true, get() throws NotFoundError on 404; otherwise it returns null"),
|
|
254
245
|
useConsoleLogger: zod.z.boolean().optional().default(false).describe("turn on console as a fallback logger"),
|
|
255
246
|
"~emitter": zod.z.any().optional().describe("emitter for events"),
|
|
256
247
|
"~normalizedLogger": zod.z.any().optional()
|
|
257
248
|
}).describe("The std config object");
|
|
258
249
|
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region impl/utils/response.mts
|
|
252
|
+
const isRecord = (value) => {
|
|
253
|
+
return value !== null && typeof value === "object";
|
|
254
|
+
};
|
|
255
|
+
const SUCCESS_STATUS_CODES = {
|
|
256
|
+
bulkGet: [200],
|
|
257
|
+
bulkSave: [201, 202],
|
|
258
|
+
changesFeed: [200],
|
|
259
|
+
database: [200],
|
|
260
|
+
documentDelete: [200, 202],
|
|
261
|
+
documentRead: [200],
|
|
262
|
+
documentWrite: [
|
|
263
|
+
200,
|
|
264
|
+
201,
|
|
265
|
+
202
|
|
266
|
+
],
|
|
267
|
+
viewQuery: [200],
|
|
268
|
+
viewStream: [200]
|
|
269
|
+
};
|
|
270
|
+
const getCouchError = (value) => {
|
|
271
|
+
if (!isRecord(value) || typeof value.error !== "string") return;
|
|
272
|
+
return value.error;
|
|
273
|
+
};
|
|
274
|
+
const isSuccessStatusCode = (profile, statusCode) => {
|
|
275
|
+
return SUCCESS_STATUS_CODES[profile].includes(statusCode);
|
|
276
|
+
};
|
|
277
|
+
|
|
259
278
|
//#endregion
|
|
260
279
|
//#region impl/utils/errors.mts
|
|
261
280
|
const RETRYABLE_STATUS_CODES = new Set([
|
|
@@ -281,6 +300,35 @@ const isNetworkError = (value) => {
|
|
|
281
300
|
const candidate = value;
|
|
282
301
|
return typeof candidate.code === "string" && candidate.code in NETWORK_ERROR_STATUS_MAP;
|
|
283
302
|
};
|
|
303
|
+
const getNestedNetworkError = (value) => {
|
|
304
|
+
if (isNetworkError(value)) return value;
|
|
305
|
+
if (typeof value !== "object" || value === null) return null;
|
|
306
|
+
const candidate = value;
|
|
307
|
+
return isNetworkError(candidate.cause) ? candidate.cause : null;
|
|
308
|
+
};
|
|
309
|
+
/**
|
|
310
|
+
* Shared base class for operational errors thrown by hide-a-bed.
|
|
311
|
+
*
|
|
312
|
+
* @public
|
|
313
|
+
*/
|
|
314
|
+
var HideABedError = class extends Error {
|
|
315
|
+
category;
|
|
316
|
+
couchError;
|
|
317
|
+
docId;
|
|
318
|
+
operation;
|
|
319
|
+
retryable;
|
|
320
|
+
statusCode;
|
|
321
|
+
constructor(message, options) {
|
|
322
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
323
|
+
this.name = "HideABedError";
|
|
324
|
+
this.category = options.category;
|
|
325
|
+
this.couchError = options.couchError;
|
|
326
|
+
this.docId = options.docId;
|
|
327
|
+
this.operation = options.operation;
|
|
328
|
+
this.retryable = options.retryable;
|
|
329
|
+
this.statusCode = options.statusCode;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
284
332
|
/**
|
|
285
333
|
* Error thrown when a requested CouchDB document cannot be found.
|
|
286
334
|
*
|
|
@@ -290,21 +338,77 @@ const isNetworkError = (value) => {
|
|
|
290
338
|
*
|
|
291
339
|
* @public
|
|
292
340
|
*/
|
|
293
|
-
var NotFoundError = class extends
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
constructor(docId, message = "Document not found") {
|
|
305
|
-
super(message);
|
|
341
|
+
var NotFoundError = class extends HideABedError {
|
|
342
|
+
constructor(docId, options = {}) {
|
|
343
|
+
super(options.message ?? "Document not found", {
|
|
344
|
+
category: "not_found",
|
|
345
|
+
couchError: options.couchError ?? "not_found",
|
|
346
|
+
cause: options.cause,
|
|
347
|
+
docId,
|
|
348
|
+
operation: options.operation,
|
|
349
|
+
retryable: false,
|
|
350
|
+
statusCode: options.statusCode ?? 404
|
|
351
|
+
});
|
|
306
352
|
this.name = "NotFoundError";
|
|
307
|
-
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Error thrown when a single-document mutation conflicts with the current revision.
|
|
357
|
+
*
|
|
358
|
+
* @public
|
|
359
|
+
*/
|
|
360
|
+
var ConflictError = class extends HideABedError {
|
|
361
|
+
constructor(docId, options = {}) {
|
|
362
|
+
super(options.message ?? "Document update conflict", {
|
|
363
|
+
category: "conflict",
|
|
364
|
+
couchError: options.couchError ?? "conflict",
|
|
365
|
+
cause: options.cause,
|
|
366
|
+
docId,
|
|
367
|
+
operation: options.operation,
|
|
368
|
+
retryable: false,
|
|
369
|
+
statusCode: options.statusCode ?? 409
|
|
370
|
+
});
|
|
371
|
+
this.name = "ConflictError";
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Error thrown when an operation fails in a non-retryable way.
|
|
376
|
+
*
|
|
377
|
+
* @public
|
|
378
|
+
*/
|
|
379
|
+
var OperationError = class extends HideABedError {
|
|
380
|
+
constructor(message, options = {}) {
|
|
381
|
+
super(message, {
|
|
382
|
+
category: options.category ?? "operation",
|
|
383
|
+
cause: options.cause,
|
|
384
|
+
couchError: options.couchError,
|
|
385
|
+
docId: options.docId,
|
|
386
|
+
operation: options.operation,
|
|
387
|
+
retryable: false,
|
|
388
|
+
statusCode: options.statusCode
|
|
389
|
+
});
|
|
390
|
+
this.name = "OperationError";
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Error thrown when schema validation fails for a document, row, key, or value.
|
|
395
|
+
*
|
|
396
|
+
* @public
|
|
397
|
+
*/
|
|
398
|
+
var ValidationError = class extends HideABedError {
|
|
399
|
+
issues;
|
|
400
|
+
constructor(options) {
|
|
401
|
+
super(options.message ?? "Validation failed", {
|
|
402
|
+
category: "validation",
|
|
403
|
+
cause: options.cause,
|
|
404
|
+
couchError: options.couchError,
|
|
405
|
+
docId: options.docId,
|
|
406
|
+
operation: options.operation,
|
|
407
|
+
retryable: false,
|
|
408
|
+
statusCode: options.statusCode
|
|
409
|
+
});
|
|
410
|
+
this.name = "ValidationError";
|
|
411
|
+
this.issues = options.issues;
|
|
308
412
|
}
|
|
309
413
|
};
|
|
310
414
|
/**
|
|
@@ -316,21 +420,18 @@ var NotFoundError = class extends Error {
|
|
|
316
420
|
*
|
|
317
421
|
* @public
|
|
318
422
|
*/
|
|
319
|
-
var RetryableError = class RetryableError extends
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
constructor(message, statusCode) {
|
|
331
|
-
super(message);
|
|
423
|
+
var RetryableError = class RetryableError extends HideABedError {
|
|
424
|
+
constructor(message, statusCode, options = {}) {
|
|
425
|
+
super(message, {
|
|
426
|
+
category: options.category ?? "retryable",
|
|
427
|
+
cause: options.cause,
|
|
428
|
+
couchError: options.couchError,
|
|
429
|
+
docId: options.docId,
|
|
430
|
+
operation: options.operation,
|
|
431
|
+
retryable: true,
|
|
432
|
+
statusCode
|
|
433
|
+
});
|
|
332
434
|
this.name = "RetryableError";
|
|
333
|
-
this.statusCode = statusCode;
|
|
334
435
|
}
|
|
335
436
|
/**
|
|
336
437
|
* Determines whether the provided status code should be treated as retryable.
|
|
@@ -351,15 +452,45 @@ var RetryableError = class RetryableError extends Error {
|
|
|
351
452
|
* @throws {@link RetryableError} When the error maps to a retryable network condition.
|
|
352
453
|
* @throws {*} Re-throws the original error when it cannot be mapped.
|
|
353
454
|
*/
|
|
354
|
-
static handleNetworkError(err) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
455
|
+
static handleNetworkError(err, operation = "request") {
|
|
456
|
+
const networkError = getNestedNetworkError(err);
|
|
457
|
+
if (networkError) {
|
|
458
|
+
const statusCode = NETWORK_ERROR_STATUS_MAP[networkError.code];
|
|
459
|
+
if (statusCode) throw new RetryableError("Network request failed", statusCode, {
|
|
460
|
+
category: "network",
|
|
461
|
+
cause: err,
|
|
462
|
+
operation
|
|
463
|
+
});
|
|
358
464
|
}
|
|
359
465
|
throw err;
|
|
360
466
|
}
|
|
361
467
|
};
|
|
468
|
+
function createResponseError({ body, defaultMessage, docId, notFoundMessage, operation, statusCode }) {
|
|
469
|
+
const couchError = getCouchError(body);
|
|
470
|
+
if (statusCode === 404 && docId) return new NotFoundError(docId, {
|
|
471
|
+
couchError,
|
|
472
|
+
message: notFoundMessage,
|
|
473
|
+
operation,
|
|
474
|
+
statusCode
|
|
475
|
+
});
|
|
476
|
+
if (statusCode === 409 && docId) return new ConflictError(docId, {
|
|
477
|
+
couchError,
|
|
478
|
+
operation,
|
|
479
|
+
statusCode
|
|
480
|
+
});
|
|
481
|
+
if (RetryableError.isRetryableStatusCode(statusCode)) return new RetryableError(defaultMessage, statusCode, {
|
|
482
|
+
couchError,
|
|
483
|
+
operation
|
|
484
|
+
});
|
|
485
|
+
return new OperationError(defaultMessage, {
|
|
486
|
+
couchError,
|
|
487
|
+
docId,
|
|
488
|
+
operation,
|
|
489
|
+
statusCode
|
|
490
|
+
});
|
|
491
|
+
}
|
|
362
492
|
function isConflictError(err) {
|
|
493
|
+
if (err instanceof ConflictError) return true;
|
|
363
494
|
if (typeof err !== "object" || err === null) return false;
|
|
364
495
|
return err.statusCode === 409;
|
|
365
496
|
}
|
|
@@ -433,27 +564,6 @@ function createLogger(config) {
|
|
|
433
564
|
return normalized;
|
|
434
565
|
}
|
|
435
566
|
|
|
436
|
-
//#endregion
|
|
437
|
-
//#region schema/util.mts
|
|
438
|
-
const MergeNeedleOpts = zod.z.function({
|
|
439
|
-
input: [CouchConfig, NeedleBaseOptions],
|
|
440
|
-
output: NeedleOptions
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
//#endregion
|
|
444
|
-
//#region impl/utils/mergeNeedleOpts.mts
|
|
445
|
-
const mergeNeedleOpts = MergeNeedleOpts.implement((config, opts) => {
|
|
446
|
-
if (config.needleOpts) return {
|
|
447
|
-
...opts,
|
|
448
|
-
...config.needleOpts,
|
|
449
|
-
headers: {
|
|
450
|
-
...opts.headers,
|
|
451
|
-
...config.needleOpts.headers ?? {}
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
return opts;
|
|
455
|
-
});
|
|
456
|
-
|
|
457
567
|
//#endregion
|
|
458
568
|
//#region schema/couch/couch.output.schema.ts
|
|
459
569
|
/**
|
|
@@ -526,8 +636,16 @@ const CouchDBInfo = zod.z.looseObject({
|
|
|
526
636
|
|
|
527
637
|
//#endregion
|
|
528
638
|
//#region impl/utils/parseRows.mts
|
|
639
|
+
const createValidationError = (issues, options) => {
|
|
640
|
+
return new ValidationError({
|
|
641
|
+
docId: options.docId,
|
|
642
|
+
issues,
|
|
643
|
+
message: options.defaultMessage ?? "Row validation failed",
|
|
644
|
+
operation: options.operation ?? "request"
|
|
645
|
+
});
|
|
646
|
+
};
|
|
529
647
|
async function parseRows(rows, options) {
|
|
530
|
-
if (!Array.isArray(rows)) throw new
|
|
648
|
+
if (!Array.isArray(rows)) throw new OperationError(options.defaultMessage ?? "Request failed", { operation: options.operation ?? "request" });
|
|
531
649
|
const isFinalRow = (row) => row !== "skip";
|
|
532
650
|
return (await Promise.all(rows.map(async (row) => {
|
|
533
651
|
try {
|
|
@@ -539,12 +657,20 @@ async function parseRows(rows, options) {
|
|
|
539
657
|
const parsedRow = zod.z.looseObject(ViewRow.shape).parse(row);
|
|
540
658
|
if (options.keySchema) {
|
|
541
659
|
const parsedKey$1 = await options.keySchema["~standard"].validate(row.key);
|
|
542
|
-
if (parsedKey$1.issues) throw parsedKey$1.issues
|
|
660
|
+
if (parsedKey$1.issues) throw createValidationError(parsedKey$1.issues, {
|
|
661
|
+
defaultMessage: options.defaultMessage,
|
|
662
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
663
|
+
operation: options.operation
|
|
664
|
+
});
|
|
543
665
|
parsedRow.key = parsedKey$1.value;
|
|
544
666
|
}
|
|
545
667
|
if (options.valueSchema) {
|
|
546
668
|
const parsedValue$1 = await options.valueSchema["~standard"].validate(row.value);
|
|
547
|
-
if (parsedValue$1.issues) throw parsedValue$1.issues
|
|
669
|
+
if (parsedValue$1.issues) throw createValidationError(parsedValue$1.issues, {
|
|
670
|
+
defaultMessage: options.defaultMessage,
|
|
671
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
672
|
+
operation: options.operation
|
|
673
|
+
});
|
|
548
674
|
parsedRow.value = parsedValue$1.value;
|
|
549
675
|
}
|
|
550
676
|
return parsedRow;
|
|
@@ -555,17 +681,29 @@ async function parseRows(rows, options) {
|
|
|
555
681
|
if (options.docSchema) {
|
|
556
682
|
const parsedDocRes = await options.docSchema["~standard"].validate(row.doc);
|
|
557
683
|
if (parsedDocRes.issues) if (options.onInvalidDoc === "skip") return "skip";
|
|
558
|
-
else throw parsedDocRes.issues
|
|
684
|
+
else throw createValidationError(parsedDocRes.issues, {
|
|
685
|
+
defaultMessage: options.defaultMessage,
|
|
686
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
687
|
+
operation: options.operation
|
|
688
|
+
});
|
|
559
689
|
else parsedDoc = parsedDocRes.value;
|
|
560
690
|
}
|
|
561
691
|
if (options.keySchema) {
|
|
562
692
|
const parsedKeyRes = await options.keySchema["~standard"].validate(row.key);
|
|
563
|
-
if (parsedKeyRes.issues) throw parsedKeyRes.issues
|
|
693
|
+
if (parsedKeyRes.issues) throw createValidationError(parsedKeyRes.issues, {
|
|
694
|
+
defaultMessage: options.defaultMessage,
|
|
695
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
696
|
+
operation: options.operation
|
|
697
|
+
});
|
|
564
698
|
else parsedKey = parsedKeyRes.value;
|
|
565
699
|
}
|
|
566
700
|
if (options.valueSchema) {
|
|
567
701
|
const parsedValueRes = await options.valueSchema["~standard"].validate(row.value);
|
|
568
|
-
if (parsedValueRes.issues) throw parsedValueRes.issues
|
|
702
|
+
if (parsedValueRes.issues) throw createValidationError(parsedValueRes.issues, {
|
|
703
|
+
defaultMessage: options.defaultMessage,
|
|
704
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
705
|
+
operation: options.operation
|
|
706
|
+
});
|
|
569
707
|
else parsedValue = parsedValueRes.value;
|
|
570
708
|
}
|
|
571
709
|
return {
|
|
@@ -581,6 +719,137 @@ async function parseRows(rows, options) {
|
|
|
581
719
|
}))).filter(isFinalRow);
|
|
582
720
|
}
|
|
583
721
|
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region impl/utils/request.mts
|
|
724
|
+
const definedSignals = (signals) => {
|
|
725
|
+
return signals.filter((signal) => signal != null);
|
|
726
|
+
};
|
|
727
|
+
const composeAbortSignal = (internalSignal, request) => {
|
|
728
|
+
const timeoutSignal = typeof request?.timeout === "number" ? AbortSignal.timeout(request.timeout) : void 0;
|
|
729
|
+
const signals = definedSignals([
|
|
730
|
+
internalSignal,
|
|
731
|
+
request?.signal,
|
|
732
|
+
timeoutSignal
|
|
733
|
+
]);
|
|
734
|
+
return {
|
|
735
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
|
|
736
|
+
timedOut: () => timeoutSignal?.aborted === true
|
|
737
|
+
};
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
//#region impl/utils/fetch.mts
|
|
742
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
743
|
+
const hasHeader = (headers, name) => {
|
|
744
|
+
const expected = name.toLowerCase();
|
|
745
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === expected);
|
|
746
|
+
};
|
|
747
|
+
const toBasicAuthHeader = ({ username, password }) => {
|
|
748
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
749
|
+
};
|
|
750
|
+
const prepareRequest = (options) => {
|
|
751
|
+
const auth = options.auth;
|
|
752
|
+
const headers = { ...options.headers ?? {} };
|
|
753
|
+
if (auth && !hasHeader(headers, "Authorization")) headers.Authorization = toBasicAuthHeader(auth);
|
|
754
|
+
return { headers };
|
|
755
|
+
};
|
|
756
|
+
const isAbortError = (err) => {
|
|
757
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
758
|
+
};
|
|
759
|
+
const isTimeoutError = (err) => {
|
|
760
|
+
return err instanceof DOMException && err.name === "TimeoutError";
|
|
761
|
+
};
|
|
762
|
+
const encodeBody = (body) => {
|
|
763
|
+
if (body == null) return void 0;
|
|
764
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof Blob || body instanceof FormData || body instanceof URLSearchParams || body instanceof ReadableStream) return body;
|
|
765
|
+
return JSON.stringify(body);
|
|
766
|
+
};
|
|
767
|
+
const parseJsonResponse = async (response) => {
|
|
768
|
+
if (response.status === 204 || response.status === 205) return null;
|
|
769
|
+
const text = await response.text();
|
|
770
|
+
if (text.trim() === "") return null;
|
|
771
|
+
try {
|
|
772
|
+
return JSON.parse(text);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
if (response.ok) throw err;
|
|
775
|
+
return text;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
async function fetchCouchJson(options) {
|
|
779
|
+
let response;
|
|
780
|
+
const { headers } = prepareRequest(options);
|
|
781
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request);
|
|
782
|
+
try {
|
|
783
|
+
response = await fetch(options.url, {
|
|
784
|
+
method: options.method,
|
|
785
|
+
headers: {
|
|
786
|
+
...JSON_HEADERS,
|
|
787
|
+
...headers
|
|
788
|
+
},
|
|
789
|
+
body: encodeBody(options.body),
|
|
790
|
+
signal,
|
|
791
|
+
dispatcher: options.request?.dispatcher
|
|
792
|
+
});
|
|
793
|
+
} catch (err) {
|
|
794
|
+
if (timedOut() || isTimeoutError(err)) throw new RetryableError("Request timed out", 503, {
|
|
795
|
+
category: "network",
|
|
796
|
+
cause: err,
|
|
797
|
+
operation: options.operation
|
|
798
|
+
});
|
|
799
|
+
if (isAbortError(err)) throw err;
|
|
800
|
+
RetryableError.handleNetworkError(err, options.operation);
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
body: await parseJsonResponse(response),
|
|
804
|
+
headers: response.headers,
|
|
805
|
+
statusCode: response.status
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async function fetchCouchStream(options) {
|
|
809
|
+
let response;
|
|
810
|
+
const { headers } = prepareRequest(options);
|
|
811
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request);
|
|
812
|
+
try {
|
|
813
|
+
response = await fetch(options.url, {
|
|
814
|
+
method: options.method,
|
|
815
|
+
headers,
|
|
816
|
+
body: encodeBody(options.body),
|
|
817
|
+
signal,
|
|
818
|
+
dispatcher: options.request?.dispatcher
|
|
819
|
+
});
|
|
820
|
+
} catch (err) {
|
|
821
|
+
if (timedOut() || isTimeoutError(err)) throw new RetryableError("Request timed out", 503, {
|
|
822
|
+
category: "network",
|
|
823
|
+
cause: err,
|
|
824
|
+
operation: options.operation
|
|
825
|
+
});
|
|
826
|
+
if (isAbortError(err)) throw err;
|
|
827
|
+
RetryableError.handleNetworkError(err, options.operation);
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
body: response.body,
|
|
831
|
+
headers: response.headers,
|
|
832
|
+
statusCode: response.status
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region impl/utils/url.mts
|
|
838
|
+
const ensureDirectoryUrl = (value) => {
|
|
839
|
+
const url = new URL(value);
|
|
840
|
+
if (!url.pathname.endsWith("/")) url.pathname = `${url.pathname}/`;
|
|
841
|
+
return url;
|
|
842
|
+
};
|
|
843
|
+
const createCouchDbUrl = (value) => {
|
|
844
|
+
return new URL(value);
|
|
845
|
+
};
|
|
846
|
+
const createCouchPathUrl = (path, base) => {
|
|
847
|
+
return new URL(path, ensureDirectoryUrl(base));
|
|
848
|
+
};
|
|
849
|
+
const createCouchDocUrl = (docId, base) => {
|
|
850
|
+
return new URL(encodeURIComponent(docId), ensureDirectoryUrl(base));
|
|
851
|
+
};
|
|
852
|
+
|
|
584
853
|
//#endregion
|
|
585
854
|
//#region impl/bulkGet.mts
|
|
586
855
|
/**
|
|
@@ -593,7 +862,7 @@ async function parseRows(rows, options) {
|
|
|
593
862
|
* @returns The raw response body from CouchDB
|
|
594
863
|
*
|
|
595
864
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
596
|
-
* @throws {
|
|
865
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
597
866
|
*/
|
|
598
867
|
async function executeBulkGet(_config, ids, includeDocs) {
|
|
599
868
|
const configParseResult = CouchConfig.safeParse(_config);
|
|
@@ -604,26 +873,35 @@ async function executeBulkGet(_config, ids, includeDocs) {
|
|
|
604
873
|
throw configParseResult.error;
|
|
605
874
|
}
|
|
606
875
|
const config = configParseResult.data;
|
|
607
|
-
const url =
|
|
876
|
+
const url = createCouchPathUrl("_all_docs", config.couch);
|
|
877
|
+
if (includeDocs) url.searchParams.append("include_docs", "true");
|
|
608
878
|
const payload = { keys: ids };
|
|
609
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
610
|
-
json: true,
|
|
611
|
-
headers: { "Content-Type": "application/json" }
|
|
612
|
-
});
|
|
613
879
|
try {
|
|
614
|
-
const resp = await (
|
|
880
|
+
const resp = await fetchCouchJson({
|
|
881
|
+
auth: config.auth,
|
|
882
|
+
method: "POST",
|
|
883
|
+
operation: "request",
|
|
884
|
+
request: config.request,
|
|
885
|
+
url,
|
|
886
|
+
body: payload
|
|
887
|
+
});
|
|
615
888
|
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
616
889
|
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
617
|
-
throw new RetryableError("
|
|
890
|
+
throw new RetryableError("Bulk get failed", resp.statusCode, { operation: "request" });
|
|
618
891
|
}
|
|
619
|
-
if (resp.statusCode
|
|
892
|
+
if (!isSuccessStatusCode("bulkGet", resp.statusCode)) {
|
|
620
893
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
621
|
-
throw
|
|
894
|
+
throw createResponseError({
|
|
895
|
+
body: resp.body,
|
|
896
|
+
defaultMessage: "Bulk get failed",
|
|
897
|
+
operation: "request",
|
|
898
|
+
statusCode: resp.statusCode
|
|
899
|
+
});
|
|
622
900
|
}
|
|
623
901
|
return resp.body;
|
|
624
902
|
} catch (err) {
|
|
625
903
|
logger.error("Network error during bulk get:", err);
|
|
626
|
-
RetryableError.handleNetworkError(err);
|
|
904
|
+
RetryableError.handleNetworkError(err, "request");
|
|
627
905
|
}
|
|
628
906
|
}
|
|
629
907
|
/**
|
|
@@ -638,16 +916,22 @@ async function executeBulkGet(_config, ids, includeDocs) {
|
|
|
638
916
|
* @returns The bulk get response with rows optionally validated against the supplied document schema.
|
|
639
917
|
*
|
|
640
918
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
641
|
-
* @throws {
|
|
642
|
-
* @throws {
|
|
919
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
920
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
643
921
|
*/
|
|
644
922
|
async function _bulkGetWithOptions(config, ids, options = {}) {
|
|
645
923
|
const body = await executeBulkGet(config, ids, options.includeDocs ?? true);
|
|
646
|
-
if (!body) throw new RetryableError("
|
|
647
|
-
if (body.error) throw
|
|
924
|
+
if (!body) throw new RetryableError("Bulk get failed", 503, { operation: "request" });
|
|
925
|
+
if (body.error) throw createResponseError({
|
|
926
|
+
body,
|
|
927
|
+
defaultMessage: "Bulk get failed",
|
|
928
|
+
operation: "request"
|
|
929
|
+
});
|
|
648
930
|
const docSchema = options.validate?.docSchema || CouchDoc;
|
|
649
931
|
const rows = await parseRows(body.rows, {
|
|
932
|
+
defaultMessage: "Bulk get failed",
|
|
650
933
|
onInvalidDoc: options.validate?.onInvalidDoc,
|
|
934
|
+
operation: "request",
|
|
651
935
|
docSchema
|
|
652
936
|
});
|
|
653
937
|
return {
|
|
@@ -673,8 +957,8 @@ async function _bulkGetWithOptions(config, ids, options = {}) {
|
|
|
673
957
|
* @returns The bulk get response with rows optionally validated against the supplied document schema.
|
|
674
958
|
*
|
|
675
959
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
676
|
-
* @throws {
|
|
677
|
-
* @throws {
|
|
960
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
961
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
678
962
|
*/
|
|
679
963
|
async function bulkGet(config, ids, options = {}) {
|
|
680
964
|
return _bulkGetWithOptions(config, ids, {
|
|
@@ -685,7 +969,7 @@ async function bulkGet(config, ids, options = {}) {
|
|
|
685
969
|
/**
|
|
686
970
|
* Bulk get documents by IDs and return a dictionary of found and not found documents.
|
|
687
971
|
*
|
|
688
|
-
* @template DocSchema - Schema used to validate each returned document, if provided. Note: if a document is found and it fails validation this will throw a
|
|
972
|
+
* @template DocSchema - Schema used to validate each returned document, if provided. Note: if a document is found and it fails validation this will throw a ValidationError.
|
|
689
973
|
*
|
|
690
974
|
* @param config - CouchDB configuration data that is validated before use.
|
|
691
975
|
* @param ids - Array of document IDs to retrieve.
|
|
@@ -694,8 +978,8 @@ async function bulkGet(config, ids, options = {}) {
|
|
|
694
978
|
* @returns An object containing found documents and not found rows.
|
|
695
979
|
*
|
|
696
980
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
697
|
-
* @throws {
|
|
698
|
-
* @throws {
|
|
981
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
982
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
699
983
|
*/
|
|
700
984
|
async function bulkGetDictionary(config, ids, options) {
|
|
701
985
|
const response = await bulkGet(config, ids, {
|
|
@@ -726,60 +1010,65 @@ async function bulkGetDictionary(config, ids, options) {
|
|
|
726
1010
|
|
|
727
1011
|
//#endregion
|
|
728
1012
|
//#region impl/get.mts
|
|
729
|
-
const ValidSchema = zod.z.custom((value) => {
|
|
1013
|
+
const ValidSchema$1 = zod.z.custom((value) => {
|
|
730
1014
|
return value !== null && typeof value === "object" && "~standard" in value;
|
|
731
1015
|
}, { message: "docSchema must be a valid StandardSchemaV1 schema" });
|
|
732
|
-
const CouchGetOptions = zod.z.
|
|
1016
|
+
const CouchGetOptions = zod.z.strictObject({
|
|
733
1017
|
rev: zod.z.string().optional().describe("the couch doc revision"),
|
|
734
|
-
validate: zod.z.object({ docSchema: ValidSchema.optional() }).optional().describe("optional document validation rules")
|
|
1018
|
+
validate: zod.z.object({ docSchema: ValidSchema$1.optional() }).optional().describe("optional document validation rules")
|
|
735
1019
|
});
|
|
736
|
-
async function _getWithOptions(
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
validate: options.validate
|
|
740
|
-
});
|
|
1020
|
+
async function _getWithOptions(configInput, id, options) {
|
|
1021
|
+
const config = CouchConfig.parse(configInput);
|
|
1022
|
+
const parsedOptions = CouchGetOptions.parse(options);
|
|
741
1023
|
const logger = createLogger(config);
|
|
742
1024
|
const rev = parsedOptions.rev;
|
|
743
|
-
const
|
|
744
|
-
const url =
|
|
745
|
-
|
|
746
|
-
json: true,
|
|
747
|
-
headers: { "Content-Type": "application/json" }
|
|
748
|
-
});
|
|
1025
|
+
const operation = rev ? "getAtRev" : "get";
|
|
1026
|
+
const url = createCouchDocUrl(id, config.couch);
|
|
1027
|
+
if (rev) url.searchParams.set("rev", rev);
|
|
749
1028
|
logger.info(`Getting document with id: ${id}, rev ${rev ?? "latest"}`);
|
|
750
1029
|
try {
|
|
751
|
-
const resp = await (
|
|
1030
|
+
const resp = await fetchCouchJson({
|
|
1031
|
+
auth: config.auth,
|
|
1032
|
+
method: "GET",
|
|
1033
|
+
operation,
|
|
1034
|
+
request: config.request,
|
|
1035
|
+
url
|
|
1036
|
+
});
|
|
752
1037
|
if (!resp) {
|
|
753
1038
|
logger.error("No response received from get request");
|
|
754
|
-
throw new RetryableError("
|
|
1039
|
+
throw new RetryableError("Request failed", 503, { operation });
|
|
755
1040
|
}
|
|
756
1041
|
const body = resp.body ?? null;
|
|
757
1042
|
if (resp.statusCode === 404) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
return null;
|
|
1043
|
+
logger.warn(`Document not found: ${id}, rev ${rev ?? "latest"}`);
|
|
1044
|
+
if (config.throwOnGetNotFound === false) return null;
|
|
1045
|
+
throw new NotFoundError(id, {
|
|
1046
|
+
operation,
|
|
1047
|
+
statusCode: resp.statusCode
|
|
1048
|
+
});
|
|
765
1049
|
}
|
|
766
|
-
if (
|
|
767
|
-
const reason = typeof body?.reason === "string" ? body.reason : "retryable error";
|
|
768
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
769
|
-
throw new RetryableError(reason, resp.statusCode);
|
|
770
|
-
}
|
|
771
|
-
if (resp.statusCode !== 200) {
|
|
772
|
-
const reason = typeof body?.reason === "string" ? body.reason : "failed";
|
|
1050
|
+
if (!isSuccessStatusCode("documentRead", resp.statusCode)) {
|
|
773
1051
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
774
|
-
throw
|
|
1052
|
+
throw createResponseError({
|
|
1053
|
+
body,
|
|
1054
|
+
defaultMessage: "Failed to fetch document",
|
|
1055
|
+
docId: id,
|
|
1056
|
+
operation,
|
|
1057
|
+
statusCode: resp.statusCode
|
|
1058
|
+
});
|
|
775
1059
|
}
|
|
776
1060
|
const typedDoc = await (parsedOptions.validate?.docSchema ?? CouchDoc)["~standard"].validate(body);
|
|
777
|
-
if (typedDoc.issues) throw
|
|
1061
|
+
if (typedDoc.issues) throw new ValidationError({
|
|
1062
|
+
docId: id,
|
|
1063
|
+
issues: typedDoc.issues,
|
|
1064
|
+
message: "Document validation failed",
|
|
1065
|
+
operation
|
|
1066
|
+
});
|
|
778
1067
|
logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? "latest"}`);
|
|
779
1068
|
return typedDoc.value;
|
|
780
1069
|
} catch (err) {
|
|
781
1070
|
logger.error("Error during get operation:", err);
|
|
782
|
-
RetryableError.handleNetworkError(err);
|
|
1071
|
+
RetryableError.handleNetworkError(err, operation);
|
|
783
1072
|
}
|
|
784
1073
|
}
|
|
785
1074
|
async function get(config, id, options) {
|
|
@@ -867,89 +1156,137 @@ function queryString(options = {}) {
|
|
|
867
1156
|
*/
|
|
868
1157
|
async function queryStream(rawConfig, view, options, onRow) {
|
|
869
1158
|
return new Promise((resolve, reject) => {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
parse_response: false
|
|
893
|
-
});
|
|
894
|
-
const parserPipeline = stream_chain.default.chain([
|
|
895
|
-
new stream_json_Parser_js.default(),
|
|
896
|
-
new stream_json_filters_Pick_js.default({ filter: "rows" }),
|
|
897
|
-
new stream_json_streamers_StreamArray_js.default()
|
|
898
|
-
]);
|
|
899
|
-
let rowCount = 0;
|
|
900
|
-
let settled = false;
|
|
901
|
-
const settleReject = (err) => {
|
|
902
|
-
if (settled) return;
|
|
903
|
-
settled = true;
|
|
904
|
-
reject(err);
|
|
905
|
-
};
|
|
906
|
-
const settleResolve = () => {
|
|
907
|
-
if (settled) return;
|
|
908
|
-
settled = true;
|
|
909
|
-
resolve();
|
|
910
|
-
};
|
|
911
|
-
let request = null;
|
|
912
|
-
parserPipeline.on("data", (chunk) => {
|
|
913
|
-
try {
|
|
914
|
-
rowCount++;
|
|
915
|
-
onRow(chunk.value);
|
|
916
|
-
} catch (callbackErr) {
|
|
917
|
-
const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr));
|
|
918
|
-
parserPipeline.destroy(error);
|
|
919
|
-
settleReject(error);
|
|
920
|
-
}
|
|
921
|
-
});
|
|
922
|
-
parserPipeline.on("error", (err) => {
|
|
923
|
-
logger.error("Stream parsing error:", err);
|
|
924
|
-
parserPipeline.destroy();
|
|
925
|
-
settleReject(new Error(`Stream parsing error: ${err.message}`, { cause: err }));
|
|
926
|
-
});
|
|
927
|
-
parserPipeline.on("end", () => {
|
|
928
|
-
logger.info(`Stream completed, processed ${rowCount} rows`);
|
|
929
|
-
settleResolve();
|
|
930
|
-
});
|
|
931
|
-
request = method === "GET" ? needle.default.get(url, mergedOpts) : needle.default.post(url, payload, mergedOpts);
|
|
932
|
-
request.on("response", (response) => {
|
|
933
|
-
logger.debug(`Received response with status code: ${response.statusCode}`);
|
|
934
|
-
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
935
|
-
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
936
|
-
settleReject(new RetryableError("retryable error during stream query", response.statusCode));
|
|
937
|
-
request.destroy();
|
|
1159
|
+
(async () => {
|
|
1160
|
+
const config = CouchConfig.parse(rawConfig);
|
|
1161
|
+
const logger = createLogger(config);
|
|
1162
|
+
logger.info(`Starting view query stream: ${view}`);
|
|
1163
|
+
const queryOptions = ViewOptions.parse(options ?? {});
|
|
1164
|
+
const request = config.request;
|
|
1165
|
+
logger.debug("Query options:", {
|
|
1166
|
+
...queryOptions,
|
|
1167
|
+
request
|
|
1168
|
+
});
|
|
1169
|
+
let method = "GET";
|
|
1170
|
+
let payload = null;
|
|
1171
|
+
let qs = queryString(queryOptions);
|
|
1172
|
+
logger.debug("Generated query string:", qs);
|
|
1173
|
+
if (typeof queryOptions.keys !== "undefined") {
|
|
1174
|
+
const MAX_URL_LENGTH = 2e3;
|
|
1175
|
+
const keysAsString = `keys=${encodeURIComponent(JSON.stringify(queryOptions.keys))}`;
|
|
1176
|
+
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) qs += (qs.length > 0 ? "&" : "") + keysAsString;
|
|
1177
|
+
else {
|
|
1178
|
+
method = "POST";
|
|
1179
|
+
payload = { keys: queryOptions.keys };
|
|
1180
|
+
}
|
|
938
1181
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1182
|
+
const url = createCouchPathUrl(view, config.couch);
|
|
1183
|
+
if (qs) url.search = qs;
|
|
1184
|
+
const requestHeaders = { "Content-Type": "application/json" };
|
|
1185
|
+
const abortController = new AbortController();
|
|
1186
|
+
const requestAbortHandler = () => {
|
|
1187
|
+
const reason = request?.signal?.reason instanceof Error ? request.signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
1188
|
+
abortController.abort(reason);
|
|
1189
|
+
responseStream?.destroy(reason);
|
|
1190
|
+
parserPipeline.destroy(reason);
|
|
1191
|
+
settleReject(reason);
|
|
1192
|
+
};
|
|
1193
|
+
const parserPipeline = stream_chain.default.chain([
|
|
1194
|
+
new stream_json_Parser_js.default(),
|
|
1195
|
+
new stream_json_filters_Pick_js.default({ filter: "rows" }),
|
|
1196
|
+
new stream_json_streamers_StreamArray_js.default()
|
|
1197
|
+
]);
|
|
1198
|
+
let rowCount = 0;
|
|
1199
|
+
let settled = false;
|
|
1200
|
+
const settleReject = (err) => {
|
|
1201
|
+
if (settled) return;
|
|
1202
|
+
settled = true;
|
|
1203
|
+
request?.signal?.removeEventListener("abort", requestAbortHandler);
|
|
1204
|
+
reject(err);
|
|
1205
|
+
};
|
|
1206
|
+
const settleResolve = () => {
|
|
1207
|
+
if (settled) return;
|
|
1208
|
+
settled = true;
|
|
1209
|
+
request?.signal?.removeEventListener("abort", requestAbortHandler);
|
|
1210
|
+
resolve();
|
|
1211
|
+
};
|
|
1212
|
+
let responseStream = null;
|
|
1213
|
+
request?.signal?.addEventListener("abort", requestAbortHandler, { once: true });
|
|
1214
|
+
parserPipeline.on("data", (chunk) => {
|
|
1215
|
+
try {
|
|
1216
|
+
rowCount++;
|
|
1217
|
+
onRow(chunk.value);
|
|
1218
|
+
} catch (callbackErr) {
|
|
1219
|
+
const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr));
|
|
1220
|
+
parserPipeline.destroy(error);
|
|
1221
|
+
settleReject(error);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
parserPipeline.on("error", (err) => {
|
|
1225
|
+
logger.error("Stream parsing error:", err);
|
|
1226
|
+
parserPipeline.destroy();
|
|
1227
|
+
settleReject(new OperationError("Stream parsing failed", {
|
|
1228
|
+
cause: err,
|
|
1229
|
+
operation: "queryStream"
|
|
1230
|
+
}));
|
|
1231
|
+
});
|
|
1232
|
+
parserPipeline.on("end", () => {
|
|
1233
|
+
logger.info(`Stream completed, processed ${rowCount} rows`);
|
|
1234
|
+
settleResolve();
|
|
1235
|
+
});
|
|
943
1236
|
try {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1237
|
+
const response = await fetchCouchStream({
|
|
1238
|
+
auth: config.auth,
|
|
1239
|
+
method,
|
|
1240
|
+
operation: "queryStream",
|
|
1241
|
+
url,
|
|
1242
|
+
body: method === "POST" ? payload : void 0,
|
|
1243
|
+
headers: requestHeaders,
|
|
1244
|
+
request,
|
|
1245
|
+
signal: abortController.signal
|
|
1246
|
+
});
|
|
1247
|
+
logger.debug(`Received response with status code: ${response.statusCode}`);
|
|
1248
|
+
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
1249
|
+
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
1250
|
+
abortController.abort();
|
|
1251
|
+
settleReject(new RetryableError("Stream query failed", response.statusCode, { operation: "queryStream" }));
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (!isSuccessStatusCode("viewStream", response.statusCode)) {
|
|
1255
|
+
abortController.abort();
|
|
1256
|
+
settleReject(createResponseError({
|
|
1257
|
+
defaultMessage: "Stream query failed",
|
|
1258
|
+
operation: "queryStream",
|
|
1259
|
+
statusCode: response.statusCode
|
|
1260
|
+
}));
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (!response.body) {
|
|
1264
|
+
settleReject(new RetryableError("Stream query failed", 503, { operation: "queryStream" }));
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
responseStream = node_stream.Readable.fromWeb(response.body);
|
|
1268
|
+
responseStream.on("error", (err) => {
|
|
1269
|
+
logger.error("Network error during stream query:", err);
|
|
1270
|
+
parserPipeline.destroy(err);
|
|
1271
|
+
try {
|
|
1272
|
+
RetryableError.handleNetworkError(err, "queryStream");
|
|
1273
|
+
} catch (retryErr) {
|
|
1274
|
+
settleReject(retryErr);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
responseStream.pipe(parserPipeline);
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
logger.error("Network error during stream query:", err);
|
|
1281
|
+
parserPipeline.destroy(err);
|
|
1282
|
+
try {
|
|
1283
|
+
RetryableError.handleNetworkError(err, "queryStream");
|
|
1284
|
+
} catch (retryErr) {
|
|
1285
|
+
settleReject(retryErr);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
950
1288
|
}
|
|
951
|
-
});
|
|
952
|
-
request.pipe(parserPipeline);
|
|
1289
|
+
})();
|
|
953
1290
|
});
|
|
954
1291
|
}
|
|
955
1292
|
|
|
@@ -958,35 +1295,46 @@ async function queryStream(rawConfig, view, options, onRow) {
|
|
|
958
1295
|
const put = async (configInput, doc) => {
|
|
959
1296
|
const config = CouchConfig.parse(configInput);
|
|
960
1297
|
const logger = createLogger(config);
|
|
961
|
-
const url =
|
|
1298
|
+
const url = createCouchDocUrl(doc._id, config.couch);
|
|
962
1299
|
const body = doc;
|
|
963
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
964
|
-
json: true,
|
|
965
|
-
headers: { "Content-Type": "application/json" }
|
|
966
|
-
});
|
|
967
1300
|
logger.info(`Putting document with id: ${doc._id}`);
|
|
968
1301
|
let resp;
|
|
969
1302
|
try {
|
|
970
|
-
resp = await (
|
|
1303
|
+
resp = await fetchCouchJson({
|
|
1304
|
+
auth: config.auth,
|
|
1305
|
+
method: "PUT",
|
|
1306
|
+
operation: "put",
|
|
1307
|
+
request: config.request,
|
|
1308
|
+
url,
|
|
1309
|
+
body
|
|
1310
|
+
});
|
|
971
1311
|
} catch (err) {
|
|
972
1312
|
logger.error("Error during put operation:", err);
|
|
973
|
-
RetryableError.handleNetworkError(err);
|
|
1313
|
+
RetryableError.handleNetworkError(err, "put");
|
|
974
1314
|
}
|
|
975
1315
|
if (!resp) {
|
|
976
1316
|
logger.error("No response received from put request");
|
|
977
|
-
throw new RetryableError("
|
|
1317
|
+
throw new RetryableError("Put failed", 503, { operation: "put" });
|
|
978
1318
|
}
|
|
979
|
-
const result = resp
|
|
1319
|
+
const result = { ...isRecord(resp.body) ? resp.body : {} };
|
|
980
1320
|
result.statusCode = resp.statusCode;
|
|
981
1321
|
if (resp.statusCode === 409) {
|
|
982
1322
|
logger.warn(`Conflict detected for document: ${doc._id}`);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1323
|
+
throw new ConflictError(doc._id, {
|
|
1324
|
+
couchError: typeof result.error === "string" ? result.error : void 0,
|
|
1325
|
+
operation: "put",
|
|
1326
|
+
statusCode: resp.statusCode
|
|
1327
|
+
});
|
|
986
1328
|
}
|
|
987
|
-
if (
|
|
988
|
-
logger.
|
|
989
|
-
throw
|
|
1329
|
+
if (!isSuccessStatusCode("documentWrite", resp.statusCode) || !result.ok) {
|
|
1330
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1331
|
+
throw createResponseError({
|
|
1332
|
+
body: resp.body,
|
|
1333
|
+
defaultMessage: "Put failed",
|
|
1334
|
+
docId: doc._id,
|
|
1335
|
+
operation: "put",
|
|
1336
|
+
statusCode: resp.statusCode
|
|
1337
|
+
});
|
|
990
1338
|
}
|
|
991
1339
|
logger.info(`Successfully saved document: ${doc._id}`);
|
|
992
1340
|
return CouchPutResponse.parse(result);
|
|
@@ -1004,7 +1352,10 @@ const PatchProperties = zod.z.looseObject({ _rev: zod.z.string("_rev is required
|
|
|
1004
1352
|
* @param _properties - Properties to merge into the document (must include _rev)
|
|
1005
1353
|
* @returns The result of the put operation
|
|
1006
1354
|
*
|
|
1007
|
-
* @throws
|
|
1355
|
+
* @throws {ConflictError} When the supplied `_rev` does not match the current document revision.
|
|
1356
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
1357
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs while reading or saving.
|
|
1358
|
+
* @throws {OperationError} When a non-retryable operational failure occurs.
|
|
1008
1359
|
*/
|
|
1009
1360
|
const patch = async (configInput, id, _properties) => {
|
|
1010
1361
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1012,12 +1363,15 @@ const patch = async (configInput, id, _properties) => {
|
|
|
1012
1363
|
const logger = createLogger(configInput);
|
|
1013
1364
|
logger.info(`Starting patch operation for document ${id}`);
|
|
1014
1365
|
logger.debug("Patch properties:", properties);
|
|
1015
|
-
const doc = await get(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1366
|
+
const doc = await get({
|
|
1367
|
+
...config,
|
|
1368
|
+
throwOnGetNotFound: true
|
|
1369
|
+
}, id);
|
|
1370
|
+
if (!doc) throw new OperationError("Patch failed", {
|
|
1371
|
+
docId: id,
|
|
1372
|
+
operation: "patch"
|
|
1373
|
+
});
|
|
1374
|
+
if (doc._rev !== properties._rev) throw new ConflictError(id, { operation: "patch" });
|
|
1021
1375
|
const updatedDoc = {
|
|
1022
1376
|
...doc,
|
|
1023
1377
|
...properties
|
|
@@ -1038,7 +1392,9 @@ const patch = async (configInput, id, _properties) => {
|
|
|
1038
1392
|
* @param properties - Properties to merge into the document
|
|
1039
1393
|
* @returns The result of the put operation or an error if max retries are exceeded
|
|
1040
1394
|
*
|
|
1041
|
-
* @throws
|
|
1395
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
1396
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs before retries are exhausted.
|
|
1397
|
+
* @throws {OperationError} When retries are exhausted or a non-retryable operational failure occurs.
|
|
1042
1398
|
*/
|
|
1043
1399
|
const patchDangerously = async (configInput, id, properties) => {
|
|
1044
1400
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1048,65 +1404,66 @@ const patchDangerously = async (configInput, id, properties) => {
|
|
|
1048
1404
|
let attempts = 0;
|
|
1049
1405
|
logger.info(`Starting patch operation for document ${id}`);
|
|
1050
1406
|
logger.debug("Patch properties:", properties);
|
|
1407
|
+
let lastError;
|
|
1051
1408
|
while (attempts <= maxRetries) {
|
|
1052
1409
|
logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`);
|
|
1053
1410
|
try {
|
|
1054
|
-
const doc = await get(
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1411
|
+
const doc = await get({
|
|
1412
|
+
...config,
|
|
1413
|
+
throwOnGetNotFound: true
|
|
1414
|
+
}, id);
|
|
1415
|
+
if (!doc) throw new OperationError("Patch failed", {
|
|
1416
|
+
docId: id,
|
|
1417
|
+
operation: "patchDangerously"
|
|
1418
|
+
});
|
|
1063
1419
|
const updatedDoc = {
|
|
1064
1420
|
...doc,
|
|
1065
1421
|
...properties
|
|
1066
1422
|
};
|
|
1067
1423
|
logger.debug("Merged document:", updatedDoc);
|
|
1068
1424
|
const result = await put(config, updatedDoc);
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
return result;
|
|
1072
|
-
}
|
|
1073
|
-
attempts++;
|
|
1074
|
-
if (attempts > maxRetries) {
|
|
1075
|
-
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`);
|
|
1076
|
-
throw new Error(`Failed to patch after ${maxRetries} attempts`);
|
|
1077
|
-
}
|
|
1078
|
-
logger.warn(`Conflict detected for ${id}, retrying (attempt ${attempts})`);
|
|
1079
|
-
await (0, node_timers_promises.setTimeout)(delay);
|
|
1080
|
-
delay *= config.backoffFactor || 2;
|
|
1081
|
-
logger.debug(`Next retry delay: ${delay}ms`);
|
|
1425
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`);
|
|
1426
|
+
return result;
|
|
1082
1427
|
} catch (err) {
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
ok: false,
|
|
1087
|
-
statusCode: 404,
|
|
1088
|
-
error: "not_found"
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1428
|
+
if (!(err instanceof Error)) throw err;
|
|
1429
|
+
if (!(err instanceof ConflictError) && !(err instanceof RetryableError)) throw err;
|
|
1430
|
+
lastError = err;
|
|
1091
1431
|
attempts++;
|
|
1092
1432
|
if (attempts > maxRetries) {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1433
|
+
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`, err);
|
|
1434
|
+
throw new OperationError("Patch failed", {
|
|
1435
|
+
cause: err,
|
|
1436
|
+
couchError: err instanceof HideABedError ? err.couchError : void 0,
|
|
1437
|
+
docId: id,
|
|
1438
|
+
operation: "patchDangerously",
|
|
1439
|
+
statusCode: err instanceof HideABedError ? err.statusCode : void 0
|
|
1440
|
+
});
|
|
1100
1441
|
}
|
|
1101
1442
|
logger.warn(`Error during patch attempt ${attempts}: ${err}`);
|
|
1102
1443
|
await (0, node_timers_promises.setTimeout)(delay);
|
|
1444
|
+
delay *= config.backoffFactor || 2;
|
|
1103
1445
|
logger.debug(`Retrying after ${delay}ms`);
|
|
1104
1446
|
}
|
|
1105
1447
|
}
|
|
1448
|
+
throw new OperationError("Patch failed", {
|
|
1449
|
+
cause: lastError,
|
|
1450
|
+
docId: id,
|
|
1451
|
+
operation: "patchDangerously"
|
|
1452
|
+
});
|
|
1106
1453
|
};
|
|
1107
1454
|
|
|
1108
1455
|
//#endregion
|
|
1109
1456
|
//#region impl/query.mts
|
|
1457
|
+
const ValidSchema = zod.z.custom((value) => {
|
|
1458
|
+
return value !== null && typeof value === "object" && "~standard" in value;
|
|
1459
|
+
}, { message: "schema must be a valid StandardSchemaV1 schema" });
|
|
1460
|
+
const QueryValidationSchema = zod.z.object({
|
|
1461
|
+
docSchema: ValidSchema.optional(),
|
|
1462
|
+
keySchema: ValidSchema.optional(),
|
|
1463
|
+
onInvalidDoc: zod.z.enum(["skip", "throw"]).optional(),
|
|
1464
|
+
valueSchema: ValidSchema.optional()
|
|
1465
|
+
}).optional();
|
|
1466
|
+
const QueryOptionsSchema = ViewOptions.extend({ validate: QueryValidationSchema }).strict();
|
|
1110
1467
|
/**
|
|
1111
1468
|
* Executes a CouchDB view query with optional schema validation and automatic handling
|
|
1112
1469
|
* of HTTP method selection, query string construction, and retryable errors.
|
|
@@ -1126,69 +1483,85 @@ const patchDangerously = async (configInput, id, properties) => {
|
|
|
1126
1483
|
* @returns The parsed view response with rows validated against the supplied schemas.
|
|
1127
1484
|
*
|
|
1128
1485
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
1129
|
-
* @throws {
|
|
1130
|
-
* @throws {
|
|
1486
|
+
* @throws {ValidationError} When row, key, value, or included document validation fails.
|
|
1487
|
+
* @throws {OperationError} When CouchDB returns a non-retryable response or malformed row payload.
|
|
1131
1488
|
*/
|
|
1132
1489
|
async function query(_config, view, options = {}) {
|
|
1133
1490
|
const configParseResult = CouchConfig.safeParse(_config);
|
|
1491
|
+
const parsedOptions = QueryOptionsSchema.parse(options || {});
|
|
1134
1492
|
const logger = createLogger(_config);
|
|
1135
1493
|
logger.info(`Starting view query: ${view}`);
|
|
1136
|
-
logger.debug("Query options:",
|
|
1494
|
+
logger.debug("Query options:", parsedOptions);
|
|
1137
1495
|
if (!configParseResult.success) {
|
|
1138
1496
|
logger.error(`Invalid configuration provided: ${zod.z.prettifyError(configParseResult.error)}`);
|
|
1139
1497
|
throw configParseResult.error;
|
|
1140
1498
|
}
|
|
1141
1499
|
const config = configParseResult.data;
|
|
1142
|
-
let qs = queryString(
|
|
1143
|
-
let method = "
|
|
1500
|
+
let qs = queryString(parsedOptions);
|
|
1501
|
+
let method = "GET";
|
|
1144
1502
|
let payload = null;
|
|
1145
|
-
|
|
1146
|
-
json: true,
|
|
1147
|
-
headers: { "Content-Type": "application/json" }
|
|
1148
|
-
});
|
|
1149
|
-
if (typeof options.keys !== "undefined") {
|
|
1503
|
+
if (typeof parsedOptions.keys !== "undefined") {
|
|
1150
1504
|
const MAX_URL_LENGTH = 2e3;
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const keysAsString = `keys=${JSON.stringify(options.keys)}`;
|
|
1505
|
+
const { keys, validate, ...queryableOptions } = parsedOptions;
|
|
1506
|
+
qs = queryString(queryableOptions);
|
|
1507
|
+
const keysAsString = `keys=${JSON.stringify(keys)}`;
|
|
1155
1508
|
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
|
|
1156
|
-
method = "
|
|
1509
|
+
method = "GET";
|
|
1157
1510
|
if (qs.length > 0) qs += "&";
|
|
1158
1511
|
else qs = "";
|
|
1159
1512
|
qs += keysAsString;
|
|
1160
1513
|
} else {
|
|
1161
|
-
method = "
|
|
1162
|
-
payload = { keys:
|
|
1514
|
+
method = "POST";
|
|
1515
|
+
payload = { keys: parsedOptions.keys };
|
|
1163
1516
|
}
|
|
1164
1517
|
}
|
|
1165
1518
|
logger.debug("Generated query string:", qs);
|
|
1166
|
-
const url =
|
|
1519
|
+
const url = createCouchPathUrl(view, config.couch);
|
|
1520
|
+
if (qs) url.search = qs;
|
|
1167
1521
|
let results;
|
|
1168
1522
|
try {
|
|
1169
1523
|
logger.debug(`Sending ${method} request to: ${url}`);
|
|
1170
|
-
results =
|
|
1524
|
+
results = await fetchCouchJson({
|
|
1525
|
+
auth: config.auth,
|
|
1526
|
+
method,
|
|
1527
|
+
operation: "query",
|
|
1528
|
+
request: config.request,
|
|
1529
|
+
url,
|
|
1530
|
+
body: method === "POST" ? payload : void 0
|
|
1531
|
+
});
|
|
1171
1532
|
} catch (err) {
|
|
1172
1533
|
logger.error("Network error during query:", err);
|
|
1173
|
-
RetryableError.handleNetworkError(err);
|
|
1534
|
+
RetryableError.handleNetworkError(err, "query");
|
|
1174
1535
|
}
|
|
1175
1536
|
if (!results) {
|
|
1176
1537
|
logger.error("No response received from query request");
|
|
1177
|
-
throw new RetryableError("
|
|
1538
|
+
throw new RetryableError("Query failed", 503, { operation: "query" });
|
|
1178
1539
|
}
|
|
1179
1540
|
const body = results.body;
|
|
1180
|
-
if (
|
|
1181
|
-
logger.
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1541
|
+
if (!isSuccessStatusCode("viewQuery", results.statusCode) || body.error) {
|
|
1542
|
+
if (body.error) logger.error(`Query error: ${JSON.stringify(body)}`);
|
|
1543
|
+
else logger.error(`Unexpected status code: ${results.statusCode}`);
|
|
1544
|
+
throw createResponseError({
|
|
1545
|
+
body,
|
|
1546
|
+
defaultMessage: "Query failed",
|
|
1547
|
+
operation: "query",
|
|
1548
|
+
statusCode: results.statusCode
|
|
1549
|
+
});
|
|
1187
1550
|
}
|
|
1188
|
-
|
|
1551
|
+
const rows = options.validate && body.rows ? await parseRows(body.rows, {
|
|
1552
|
+
...options.validate,
|
|
1553
|
+
defaultMessage: "Query failed",
|
|
1554
|
+
operation: "query"
|
|
1555
|
+
}) : body.rows ?? [];
|
|
1189
1556
|
logger.info(`Successfully executed view query: ${view}`);
|
|
1190
|
-
logger.debug("Query response:",
|
|
1191
|
-
|
|
1557
|
+
logger.debug("Query response:", {
|
|
1558
|
+
...body,
|
|
1559
|
+
rows
|
|
1560
|
+
});
|
|
1561
|
+
return {
|
|
1562
|
+
...body,
|
|
1563
|
+
rows
|
|
1564
|
+
};
|
|
1192
1565
|
}
|
|
1193
1566
|
|
|
1194
1567
|
//#endregion
|
|
@@ -1200,35 +1573,35 @@ const setupEmitter = (config) => {
|
|
|
1200
1573
|
|
|
1201
1574
|
//#endregion
|
|
1202
1575
|
//#region impl/utils/transactionErrors.mts
|
|
1203
|
-
var TransactionSetupError = class extends
|
|
1576
|
+
var TransactionSetupError = class extends OperationError {
|
|
1204
1577
|
details;
|
|
1205
1578
|
constructor(message, details = {}) {
|
|
1206
|
-
super(message);
|
|
1579
|
+
super(message, { category: "transaction" });
|
|
1207
1580
|
this.name = "TransactionSetupError";
|
|
1208
1581
|
this.details = details;
|
|
1209
1582
|
}
|
|
1210
1583
|
};
|
|
1211
|
-
var TransactionVersionConflictError = class extends
|
|
1584
|
+
var TransactionVersionConflictError = class extends OperationError {
|
|
1212
1585
|
conflictingIds;
|
|
1213
1586
|
constructor(conflictingIds) {
|
|
1214
|
-
super(`Revision mismatch for documents: ${conflictingIds.join(", ")}
|
|
1587
|
+
super(`Revision mismatch for documents: ${conflictingIds.join(", ")}`, { category: "transaction" });
|
|
1215
1588
|
this.name = "TransactionVersionConflictError";
|
|
1216
1589
|
this.conflictingIds = conflictingIds;
|
|
1217
1590
|
}
|
|
1218
1591
|
};
|
|
1219
|
-
var TransactionBulkOperationError = class extends
|
|
1592
|
+
var TransactionBulkOperationError = class extends OperationError {
|
|
1220
1593
|
failedDocs;
|
|
1221
1594
|
constructor(failedDocs) {
|
|
1222
|
-
super(`Failed to save documents: ${failedDocs.map((d) => d.id).join(", ")}
|
|
1595
|
+
super(`Failed to save documents: ${failedDocs.map((d) => d.id).join(", ")}`, { category: "transaction" });
|
|
1223
1596
|
this.name = "TransactionBulkOperationError";
|
|
1224
1597
|
this.failedDocs = failedDocs;
|
|
1225
1598
|
}
|
|
1226
1599
|
};
|
|
1227
|
-
var TransactionRollbackError = class extends
|
|
1600
|
+
var TransactionRollbackError = class extends OperationError {
|
|
1228
1601
|
originalError;
|
|
1229
1602
|
rollbackResults;
|
|
1230
1603
|
constructor(message, originalError, rollbackResults) {
|
|
1231
|
-
super(message);
|
|
1604
|
+
super(message, { category: "transaction" });
|
|
1232
1605
|
this.name = "TransactionRollbackError";
|
|
1233
1606
|
this.originalError = originalError;
|
|
1234
1607
|
this.rollbackResults = rollbackResults;
|
|
@@ -1248,39 +1621,48 @@ var TransactionRollbackError = class extends Error {
|
|
|
1248
1621
|
* @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
|
|
1249
1622
|
*
|
|
1250
1623
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
1251
|
-
* @throws {
|
|
1624
|
+
* @throws {OperationError} When bulk save input is invalid or CouchDB returns a non-retryable request-level failure.
|
|
1252
1625
|
*/
|
|
1253
1626
|
const bulkSave = async (config, docs) => {
|
|
1254
|
-
const
|
|
1627
|
+
const parsedConfig = CouchConfig.parse(config);
|
|
1628
|
+
const logger = createLogger(parsedConfig);
|
|
1255
1629
|
if (docs == null || !docs.length) {
|
|
1256
1630
|
logger.error("bulkSave called with no docs");
|
|
1257
|
-
throw new
|
|
1631
|
+
throw new OperationError("Bulk save requires at least one document", { operation: "request" });
|
|
1258
1632
|
}
|
|
1259
1633
|
logger.info(`Starting bulk save of ${docs.length} documents`);
|
|
1260
|
-
const url =
|
|
1634
|
+
const url = createCouchPathUrl("_bulk_docs", parsedConfig.couch);
|
|
1261
1635
|
const body = { docs };
|
|
1262
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
1263
|
-
json: true,
|
|
1264
|
-
headers: { "Content-Type": "application/json" }
|
|
1265
|
-
});
|
|
1266
1636
|
let resp;
|
|
1267
1637
|
try {
|
|
1268
|
-
resp = await (
|
|
1638
|
+
resp = await fetchCouchJson({
|
|
1639
|
+
auth: parsedConfig.auth,
|
|
1640
|
+
method: "POST",
|
|
1641
|
+
operation: "request",
|
|
1642
|
+
request: parsedConfig.request,
|
|
1643
|
+
url,
|
|
1644
|
+
body
|
|
1645
|
+
});
|
|
1269
1646
|
} catch (err) {
|
|
1270
1647
|
logger.error("Network error during bulk save:", err);
|
|
1271
|
-
RetryableError.handleNetworkError(err);
|
|
1648
|
+
RetryableError.handleNetworkError(err, "request");
|
|
1272
1649
|
}
|
|
1273
1650
|
if (!resp) {
|
|
1274
1651
|
logger.error("No response received from bulk save request");
|
|
1275
|
-
throw new RetryableError("
|
|
1652
|
+
throw new RetryableError("Bulk save failed", 503, { operation: "request" });
|
|
1276
1653
|
}
|
|
1277
1654
|
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1278
1655
|
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1279
|
-
throw new RetryableError("
|
|
1656
|
+
throw new RetryableError("Bulk save failed", resp.statusCode, { operation: "request" });
|
|
1280
1657
|
}
|
|
1281
|
-
if (resp.statusCode
|
|
1658
|
+
if (!isSuccessStatusCode("bulkSave", resp.statusCode)) {
|
|
1282
1659
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1283
|
-
throw
|
|
1660
|
+
throw createResponseError({
|
|
1661
|
+
body: resp.body,
|
|
1662
|
+
defaultMessage: "Bulk save failed",
|
|
1663
|
+
operation: "request",
|
|
1664
|
+
statusCode: resp.statusCode
|
|
1665
|
+
});
|
|
1284
1666
|
}
|
|
1285
1667
|
const results = resp?.body || [];
|
|
1286
1668
|
return BulkSaveResponse.parse(results);
|
|
@@ -1343,16 +1725,21 @@ const bulkSaveTransaction = async (config, transactionId, docs) => {
|
|
|
1343
1725
|
changes: docs,
|
|
1344
1726
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1345
1727
|
};
|
|
1346
|
-
let transactionResponse
|
|
1728
|
+
let transactionResponse;
|
|
1729
|
+
try {
|
|
1730
|
+
transactionResponse = await _put(transactionDoc);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
if (error instanceof ConflictError) throw new TransactionSetupError("Failed to create transaction document", {
|
|
1733
|
+
error: error.couchError,
|
|
1734
|
+
response: error
|
|
1735
|
+
});
|
|
1736
|
+
throw error;
|
|
1737
|
+
}
|
|
1347
1738
|
logger.debug("Transaction document created:", transactionDoc, transactionResponse);
|
|
1348
1739
|
await emitter.emit("transaction-created", {
|
|
1349
1740
|
transactionResponse,
|
|
1350
1741
|
txnDoc: transactionDoc
|
|
1351
1742
|
});
|
|
1352
|
-
if (transactionResponse.error) throw new TransactionSetupError("Failed to create transaction document", {
|
|
1353
|
-
error: transactionResponse.error,
|
|
1354
|
-
response: transactionResponse
|
|
1355
|
-
});
|
|
1356
1743
|
const existingDocs = await bulkGetDictionary(config, docs.map((d) => d._id));
|
|
1357
1744
|
logger.debug("Fetched current revisions of documents:", existingDocs);
|
|
1358
1745
|
await emitter.emit("transaction-revs-fetched", existingDocs);
|
|
@@ -1445,44 +1832,44 @@ const bulkSaveTransaction = async (config, transactionId, docs) => {
|
|
|
1445
1832
|
const remove = async (configInput, id, rev) => {
|
|
1446
1833
|
const config = CouchConfig.parse(configInput);
|
|
1447
1834
|
const logger = createLogger(config);
|
|
1448
|
-
const url =
|
|
1449
|
-
|
|
1450
|
-
json: true,
|
|
1451
|
-
headers: { "Content-Type": "application/json" }
|
|
1452
|
-
});
|
|
1835
|
+
const url = createCouchDocUrl(id, config.couch);
|
|
1836
|
+
url.searchParams.set("rev", rev);
|
|
1453
1837
|
logger.info(`Deleting document with id: ${id}`);
|
|
1454
1838
|
let resp;
|
|
1455
1839
|
try {
|
|
1456
|
-
resp = await (
|
|
1840
|
+
resp = await fetchCouchJson({
|
|
1841
|
+
auth: config.auth,
|
|
1842
|
+
method: "DELETE",
|
|
1843
|
+
operation: "remove",
|
|
1844
|
+
request: config.request,
|
|
1845
|
+
url
|
|
1846
|
+
});
|
|
1457
1847
|
} catch (err) {
|
|
1458
1848
|
logger.error("Error during delete operation:", err);
|
|
1459
|
-
RetryableError.handleNetworkError(err);
|
|
1849
|
+
RetryableError.handleNetworkError(err, "remove");
|
|
1460
1850
|
}
|
|
1461
1851
|
if (!resp) {
|
|
1462
1852
|
logger.error("No response received from delete request");
|
|
1463
|
-
throw new RetryableError("
|
|
1464
|
-
}
|
|
1465
|
-
let result;
|
|
1466
|
-
if (typeof resp.body === "string") try {
|
|
1467
|
-
result = JSON.parse(resp.body);
|
|
1468
|
-
} catch {
|
|
1469
|
-
result = {};
|
|
1853
|
+
throw new RetryableError("Remove failed", 503, { operation: "remove" });
|
|
1470
1854
|
}
|
|
1471
|
-
|
|
1855
|
+
const result = { ...isRecord(resp.body) ? resp.body : {} };
|
|
1472
1856
|
result.statusCode = resp.statusCode;
|
|
1473
1857
|
if (resp.statusCode === 404) {
|
|
1474
1858
|
logger.warn(`Document not found for deletion: ${id}`);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1480
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1481
|
-
throw new RetryableError(result.reason || "retryable error", resp.statusCode);
|
|
1859
|
+
throw new NotFoundError(id, {
|
|
1860
|
+
operation: "remove",
|
|
1861
|
+
statusCode: resp.statusCode
|
|
1862
|
+
});
|
|
1482
1863
|
}
|
|
1483
|
-
if (resp.statusCode
|
|
1864
|
+
if (!isSuccessStatusCode("documentDelete", resp.statusCode) || !result.ok) {
|
|
1484
1865
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1485
|
-
throw
|
|
1866
|
+
throw createResponseError({
|
|
1867
|
+
body: resp.body,
|
|
1868
|
+
defaultMessage: "Remove failed",
|
|
1869
|
+
docId: id,
|
|
1870
|
+
operation: "remove",
|
|
1871
|
+
statusCode: resp.statusCode
|
|
1872
|
+
});
|
|
1486
1873
|
}
|
|
1487
1874
|
logger.info(`Successfully deleted document: ${id}`);
|
|
1488
1875
|
return CouchPutResponse.parse(result);
|
|
@@ -1512,7 +1899,8 @@ const remove = async (configInput, id, rev) => {
|
|
|
1512
1899
|
* console.log(results);
|
|
1513
1900
|
* ```
|
|
1514
1901
|
*
|
|
1515
|
-
* @throws
|
|
1902
|
+
* @throws {RetryableError} When the bulk request fails with a retryable transport or HTTP error.
|
|
1903
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
1516
1904
|
*/
|
|
1517
1905
|
const bulkRemove = async (configInput, ids) => {
|
|
1518
1906
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1554,7 +1942,8 @@ const bulkRemove = async (configInput, ids) => {
|
|
|
1554
1942
|
* console.log(results);
|
|
1555
1943
|
* ```
|
|
1556
1944
|
*
|
|
1557
|
-
* @throws
|
|
1945
|
+
* @throws {RetryableError} When a request-level transport or retryable HTTP failure occurs.
|
|
1946
|
+
* @throws {OperationError} When a request fails in a non-retryable way.
|
|
1558
1947
|
*/
|
|
1559
1948
|
const bulkRemoveMap = async (configInput, ids) => {
|
|
1560
1949
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1583,7 +1972,7 @@ const bulkRemoveMap = async (configInput, ids) => {
|
|
|
1583
1972
|
* @param configInput - The CouchDB configuration input.
|
|
1584
1973
|
* @returns A promise that resolves to the CouchDB database information.
|
|
1585
1974
|
* @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
|
|
1586
|
-
* @throws {
|
|
1975
|
+
* @throws {OperationError} `OperationError` For other non-retryable response failures.
|
|
1587
1976
|
*
|
|
1588
1977
|
* @example
|
|
1589
1978
|
* ```ts
|
|
@@ -1603,27 +1992,34 @@ const bulkRemoveMap = async (configInput, ids) => {
|
|
|
1603
1992
|
const getDBInfo = async (configInput) => {
|
|
1604
1993
|
const config = CouchConfig.parse(configInput);
|
|
1605
1994
|
const logger = createLogger(config);
|
|
1606
|
-
const url =
|
|
1995
|
+
const url = createCouchDbUrl(config.couch);
|
|
1607
1996
|
let resp;
|
|
1608
1997
|
try {
|
|
1609
|
-
resp = await (
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1998
|
+
resp = await fetchCouchJson({
|
|
1999
|
+
auth: config.auth,
|
|
2000
|
+
method: "GET",
|
|
2001
|
+
operation: "getDBInfo",
|
|
2002
|
+
request: config.request,
|
|
2003
|
+
url
|
|
2004
|
+
});
|
|
2005
|
+
if (!isSuccessStatusCode("database", resp.statusCode)) {
|
|
2006
|
+
logger.error(`Non-success status code received: ${resp.statusCode}`);
|
|
2007
|
+
throw createResponseError({
|
|
2008
|
+
body: resp.body,
|
|
2009
|
+
defaultMessage: "Failed to fetch database info",
|
|
2010
|
+
operation: "getDBInfo",
|
|
2011
|
+
statusCode: resp.statusCode
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
1613
2014
|
} catch (err) {
|
|
1614
2015
|
logger.error("Error during get operation:", err);
|
|
1615
|
-
RetryableError.handleNetworkError(err);
|
|
2016
|
+
RetryableError.handleNetworkError(err, "getDBInfo");
|
|
1616
2017
|
}
|
|
1617
2018
|
if (!resp) {
|
|
1618
2019
|
logger.error("No response received from get request");
|
|
1619
|
-
throw new RetryableError("
|
|
2020
|
+
throw new RetryableError("Failed to fetch database info", 503, { operation: "getDBInfo" });
|
|
1620
2021
|
}
|
|
1621
|
-
|
|
1622
|
-
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1623
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1624
|
-
throw new RetryableError(result.reason ?? "retryable error", resp.statusCode);
|
|
1625
|
-
}
|
|
1626
|
-
return CouchDBInfo.parse(result);
|
|
2022
|
+
return CouchDBInfo.parse(resp.body);
|
|
1627
2023
|
};
|
|
1628
2024
|
|
|
1629
2025
|
//#endregion
|
|
@@ -1719,7 +2115,7 @@ async function removeLock(configInput, docId, lockOptions) {
|
|
|
1719
2115
|
|
|
1720
2116
|
//#endregion
|
|
1721
2117
|
//#region schema/sugar/watch.mts
|
|
1722
|
-
const WatchOptions = zod.z.
|
|
2118
|
+
const WatchOptions = zod.z.strictObject({
|
|
1723
2119
|
include_docs: zod.z.boolean().default(false),
|
|
1724
2120
|
maxRetries: zod.z.number().describe("maximum number of retries before giving up"),
|
|
1725
2121
|
initialDelay: zod.z.number().describe("initial delay between retries in milliseconds"),
|
|
@@ -1745,112 +2141,155 @@ function watchDocs(configInput, docIds, onChange, optionsInput = {}) {
|
|
|
1745
2141
|
const options = WatchOptions.parse(optionsInput);
|
|
1746
2142
|
const logger = createLogger(config);
|
|
1747
2143
|
const emitter = new events.EventEmitter();
|
|
2144
|
+
const request = config.request;
|
|
1748
2145
|
let lastSeq = null;
|
|
1749
2146
|
let stopping = false;
|
|
2147
|
+
let stopEndEmitted = false;
|
|
1750
2148
|
let retryCount = 0;
|
|
1751
|
-
|
|
2149
|
+
const lifecycleAbortController = new AbortController();
|
|
2150
|
+
let currentAbortController = null;
|
|
1752
2151
|
const maxRetries = options.maxRetries || 10;
|
|
1753
2152
|
const initialDelay = options.initialDelay || 1e3;
|
|
1754
2153
|
const maxDelay = options.maxDelay || 3e4;
|
|
1755
2154
|
const _docIds = Array.isArray(docIds) ? docIds : [docIds];
|
|
1756
|
-
if (_docIds.length === 0) throw new
|
|
1757
|
-
if (_docIds.length > 100) throw new
|
|
2155
|
+
if (_docIds.length === 0) throw new OperationError("docIds must be a non-empty array", { operation: "watchDocs" });
|
|
2156
|
+
if (_docIds.length > 100) throw new OperationError("docIds must be an array of 100 or fewer elements", { operation: "watchDocs" });
|
|
2157
|
+
const emitStopEnd = () => {
|
|
2158
|
+
if (stopEndEmitted) return;
|
|
2159
|
+
stopEndEmitted = true;
|
|
2160
|
+
emitter.emit("end", { lastSeq });
|
|
2161
|
+
};
|
|
2162
|
+
const stopWatching = () => {
|
|
2163
|
+
if (stopping) return;
|
|
2164
|
+
stopping = true;
|
|
2165
|
+
lifecycleAbortController.abort();
|
|
2166
|
+
currentAbortController?.abort();
|
|
2167
|
+
request?.signal?.removeEventListener("abort", handleExternalAbort);
|
|
2168
|
+
emitStopEnd();
|
|
2169
|
+
emitter.removeAllListeners();
|
|
2170
|
+
};
|
|
2171
|
+
const handleExternalAbort = () => {
|
|
2172
|
+
logger.info(`Request signal aborted, stopping watcher for [${_docIds}]`);
|
|
2173
|
+
stopWatching();
|
|
2174
|
+
};
|
|
2175
|
+
request?.signal?.addEventListener("abort", handleExternalAbort, { once: true });
|
|
1758
2176
|
const connect = async () => {
|
|
1759
2177
|
if (stopping) return;
|
|
1760
2178
|
const feed = "continuous";
|
|
1761
2179
|
const includeDocs = options.include_docs ?? false;
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2180
|
+
const url = createCouchPathUrl("_changes", config.couch);
|
|
2181
|
+
url.searchParams.set("feed", feed);
|
|
2182
|
+
url.searchParams.set("since", String(lastSeq));
|
|
2183
|
+
url.searchParams.set("include_docs", String(includeDocs));
|
|
2184
|
+
url.searchParams.set("filter", "_doc_ids");
|
|
2185
|
+
url.searchParams.set("doc_ids", JSON.stringify(_docIds));
|
|
2186
|
+
const abortController = new AbortController();
|
|
2187
|
+
currentAbortController = abortController;
|
|
1769
2188
|
let buffer = "";
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const lines = buffer.split("\n");
|
|
1774
|
-
buffer = lines.pop() || "";
|
|
1775
|
-
for (const line of lines) if (line.trim()) try {
|
|
2189
|
+
const processLine = (line) => {
|
|
2190
|
+
if (!line.trim()) return;
|
|
2191
|
+
try {
|
|
1776
2192
|
const change = JSON.parse(line);
|
|
1777
|
-
if (!change.id) return
|
|
2193
|
+
if (!change.id) return;
|
|
1778
2194
|
logger.debug(`Change detected, watching [${_docIds}]`, change);
|
|
1779
2195
|
lastSeq = change.seq || change.last_seq;
|
|
1780
2196
|
emitter.emit("change", change);
|
|
1781
2197
|
} catch (err) {
|
|
1782
2198
|
logger.error("Error parsing change:", err, "Line:", line);
|
|
1783
2199
|
}
|
|
1784
|
-
}
|
|
1785
|
-
|
|
2200
|
+
};
|
|
2201
|
+
try {
|
|
2202
|
+
const response = await fetchCouchStream({
|
|
2203
|
+
auth: config.auth,
|
|
2204
|
+
method: "GET",
|
|
2205
|
+
operation: "watchDocs",
|
|
2206
|
+
url,
|
|
2207
|
+
headers: { "Content-Type": "application/json" },
|
|
2208
|
+
request,
|
|
2209
|
+
signal: AbortSignal.any([abortController.signal, lifecycleAbortController.signal])
|
|
2210
|
+
});
|
|
1786
2211
|
logger.debug(`Received response with status code, watching [${_docIds}]: ${response.statusCode}`);
|
|
1787
2212
|
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
1788
2213
|
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
1789
|
-
|
|
1790
|
-
handleReconnect();
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
2214
|
+
abortController.abort();
|
|
2215
|
+
await handleReconnect();
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (!isSuccessStatusCode("changesFeed", response.statusCode)) {
|
|
2219
|
+
emitter.emit("error", createResponseError({
|
|
2220
|
+
defaultMessage: "Watch request failed",
|
|
2221
|
+
operation: "watchDocs",
|
|
2222
|
+
statusCode: response.statusCode
|
|
2223
|
+
}));
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
retryCount = 0;
|
|
2227
|
+
if (!response.body) throw new RetryableError("Watch request failed", 503, { operation: "watchDocs" });
|
|
2228
|
+
const reader = response.body.getReader();
|
|
2229
|
+
const decoder = new TextDecoder();
|
|
2230
|
+
while (!stopping) {
|
|
2231
|
+
const { done, value } = await reader.read();
|
|
2232
|
+
if (done) break;
|
|
2233
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2234
|
+
const lines = buffer.split("\n");
|
|
2235
|
+
buffer = lines.pop() || "";
|
|
2236
|
+
lines.forEach(processLine);
|
|
2237
|
+
}
|
|
2238
|
+
if (stopping || abortController.signal.aborted) return;
|
|
2239
|
+
buffer += decoder.decode();
|
|
2240
|
+
if (buffer.trim()) processLine(buffer);
|
|
2241
|
+
logger.info("Stream completed. Last seen seq: ", lastSeq);
|
|
2242
|
+
emitter.emit("end", { lastSeq });
|
|
2243
|
+
if (!stopping) await handleReconnect();
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
if (stopping || abortController.signal.aborted) {
|
|
1795
2246
|
logger.info("stopping in progress, ignore stream error");
|
|
1796
2247
|
return;
|
|
1797
2248
|
}
|
|
1798
|
-
logger.error(`Network error during stream, watching [${_docIds}]:`, err
|
|
2249
|
+
logger.error(`Network error during stream, watching [${_docIds}]:`, String(err));
|
|
1799
2250
|
try {
|
|
1800
|
-
RetryableError.handleNetworkError(err);
|
|
2251
|
+
RetryableError.handleNetworkError(err, "watchDocs");
|
|
1801
2252
|
} catch (filteredError) {
|
|
1802
2253
|
if (filteredError instanceof RetryableError) {
|
|
1803
2254
|
logger.info(`Retryable error, watching [${_docIds}]:`, filteredError.toString());
|
|
1804
|
-
handleReconnect();
|
|
2255
|
+
await handleReconnect();
|
|
1805
2256
|
} else {
|
|
1806
|
-
logger.error(`Non-retryable error, watching [${_docIds}]`, filteredError
|
|
2257
|
+
logger.error(`Non-retryable error, watching [${_docIds}]`, String(filteredError));
|
|
1807
2258
|
emitter.emit("error", filteredError);
|
|
1808
2259
|
}
|
|
1809
2260
|
}
|
|
1810
|
-
}
|
|
1811
|
-
currentRequest.on("end", () => {
|
|
1812
|
-
if (buffer.trim()) try {
|
|
1813
|
-
const change = JSON.parse(buffer);
|
|
1814
|
-
logger.debug("Final change detected:", change);
|
|
1815
|
-
emitter.emit("change", change);
|
|
1816
|
-
} catch (err) {
|
|
1817
|
-
logger.error("Error parsing final change:", err);
|
|
1818
|
-
}
|
|
1819
|
-
logger.info("Stream completed. Last seen seq: ", lastSeq);
|
|
1820
|
-
emitter.emit("end", { lastSeq });
|
|
1821
|
-
if (!stopping) handleReconnect();
|
|
1822
|
-
});
|
|
2261
|
+
}
|
|
1823
2262
|
};
|
|
1824
2263
|
const handleReconnect = async () => {
|
|
1825
2264
|
if (stopping || retryCount >= maxRetries) {
|
|
1826
2265
|
if (retryCount >= maxRetries) {
|
|
1827
2266
|
logger.error(`Max retries (${maxRetries}) reached, giving up`);
|
|
1828
|
-
emitter.emit("error",
|
|
2267
|
+
emitter.emit("error", new OperationError("Watch retries exhausted", { operation: "watchDocs" }));
|
|
1829
2268
|
}
|
|
1830
2269
|
return;
|
|
1831
2270
|
}
|
|
1832
2271
|
const delay = Math.min(initialDelay * Math.pow(2, retryCount), maxDelay);
|
|
1833
2272
|
retryCount++;
|
|
1834
2273
|
logger.info(`Attempting to reconnect in ${delay}ms (attempt ${retryCount} of ${maxRetries})`);
|
|
1835
|
-
await (0, node_timers_promises.setTimeout)(delay);
|
|
1836
2274
|
try {
|
|
1837
|
-
|
|
2275
|
+
await (0, node_timers_promises.setTimeout)(delay, void 0, { signal: lifecycleAbortController.signal });
|
|
2276
|
+
} catch {
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
await connect();
|
|
1838
2281
|
} catch (err) {
|
|
1839
2282
|
logger.error("Error during reconnection:", err);
|
|
1840
|
-
handleReconnect();
|
|
2283
|
+
await handleReconnect();
|
|
1841
2284
|
}
|
|
1842
2285
|
};
|
|
1843
|
-
connect();
|
|
1844
2286
|
emitter.on("change", onChange);
|
|
2287
|
+
if (request?.signal?.aborted) stopWatching();
|
|
2288
|
+
else connect();
|
|
1845
2289
|
return {
|
|
1846
2290
|
on: (event, listener) => emitter.on(event, listener),
|
|
1847
2291
|
removeListener: (event, listener) => emitter.removeListener(event, listener),
|
|
1848
|
-
stop:
|
|
1849
|
-
stopping = true;
|
|
1850
|
-
if (currentRequest) currentRequest.destroy();
|
|
1851
|
-
emitter.emit("end", { lastSeq });
|
|
1852
|
-
emitter.removeAllListeners();
|
|
1853
|
-
}
|
|
2292
|
+
stop: stopWatching
|
|
1854
2293
|
};
|
|
1855
2294
|
}
|
|
1856
2295
|
|
|
@@ -1928,7 +2367,13 @@ function doBind(config) {
|
|
|
1928
2367
|
}
|
|
1929
2368
|
|
|
1930
2369
|
//#endregion
|
|
2370
|
+
exports.ConflictError = ConflictError;
|
|
2371
|
+
exports.HideABedError = HideABedError;
|
|
2372
|
+
exports.NotFoundError = NotFoundError;
|
|
2373
|
+
exports.OperationError = OperationError;
|
|
1931
2374
|
exports.QueryBuilder = QueryBuilder;
|
|
2375
|
+
exports.RetryableError = RetryableError;
|
|
2376
|
+
exports.ValidationError = ValidationError;
|
|
1932
2377
|
exports.bindConfig = bindConfig;
|
|
1933
2378
|
exports.bulkGet = bulkGet;
|
|
1934
2379
|
exports.bulkGetDictionary = bulkGetDictionary;
|