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/esm/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import z, { z as z$1 } from "zod";
|
|
2
2
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
3
|
-
import
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
4
|
import Chain from "stream-chain";
|
|
5
5
|
import Parser from "stream-json/Parser.js";
|
|
6
6
|
import Pick from "stream-json/filters/Pick.js";
|
|
@@ -149,6 +149,21 @@ var QueryBuilder = class {
|
|
|
149
149
|
};
|
|
150
150
|
const createQuery = () => new QueryBuilder();
|
|
151
151
|
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region schema/request.mts
|
|
154
|
+
const isAbortSignal = (value) => {
|
|
155
|
+
return value instanceof AbortSignal;
|
|
156
|
+
};
|
|
157
|
+
const isDispatcher = (value) => {
|
|
158
|
+
if (typeof value !== "object" || value === null) return false;
|
|
159
|
+
return typeof value.dispatch === "function";
|
|
160
|
+
};
|
|
161
|
+
const RequestOptions = z$1.strictObject({
|
|
162
|
+
dispatcher: z$1.custom(isDispatcher, { message: "dispatcher must expose a dispatch method" }).optional().describe("dispatcher to use for the request"),
|
|
163
|
+
signal: z$1.custom(isAbortSignal, { message: "signal must be an AbortSignal" }).optional().describe("abort signal for the request"),
|
|
164
|
+
timeout: z$1.number().nonnegative().optional().describe("request timeout in milliseconds")
|
|
165
|
+
});
|
|
166
|
+
|
|
152
167
|
//#endregion
|
|
153
168
|
//#region schema/config.mts
|
|
154
169
|
const anyArgs = z$1.array(z$1.any());
|
|
@@ -173,56 +188,61 @@ const LoggerSchema = z$1.object({
|
|
|
173
188
|
input: anyArgs,
|
|
174
189
|
output: z$1.void()
|
|
175
190
|
}));
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
parse_response: z$1.boolean().optional()
|
|
191
|
+
const CouchAuth = z$1.strictObject({
|
|
192
|
+
username: z$1.string().describe("basic auth username for CouchDB requests"),
|
|
193
|
+
password: z$1.string().describe("basic auth password for CouchDB requests")
|
|
180
194
|
});
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
timeout: z$1.number().optional(),
|
|
189
|
-
read_timeout: z$1.number().optional(),
|
|
190
|
-
parse_response: z$1.boolean().optional(),
|
|
191
|
-
decode: z$1.boolean().optional(),
|
|
192
|
-
parse_cookies: z$1.boolean().optional(),
|
|
193
|
-
cookies: z$1.record(z$1.string(), z$1.string()).optional(),
|
|
194
|
-
headers: z$1.record(z$1.string(), z$1.string()).optional(),
|
|
195
|
-
auth: z$1.enum([
|
|
196
|
-
"auto",
|
|
197
|
-
"digest",
|
|
198
|
-
"basic"
|
|
199
|
-
]).optional(),
|
|
200
|
-
username: z$1.string().optional(),
|
|
201
|
-
password: z$1.string().optional(),
|
|
202
|
-
proxy: z$1.string().optional(),
|
|
203
|
-
agent: z$1.any().optional(),
|
|
204
|
-
rejectUnauthorized: z$1.boolean().optional(),
|
|
205
|
-
output: z$1.string().optional(),
|
|
206
|
-
parse: z$1.boolean().optional(),
|
|
207
|
-
multipart: z$1.boolean().optional(),
|
|
208
|
-
open_timeout: z$1.number().optional(),
|
|
209
|
-
response_timeout: z$1.number().optional(),
|
|
210
|
-
keepAlive: z$1.boolean().optional()
|
|
195
|
+
const CouchUrl = z$1.custom((value) => {
|
|
196
|
+
try {
|
|
197
|
+
const url = new URL(value);
|
|
198
|
+
return url.username === "" && url.password === "";
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
211
202
|
});
|
|
212
203
|
const CouchConfig = z$1.strictObject({
|
|
204
|
+
auth: CouchAuth.optional().describe("basic auth credentials for CouchDB requests"),
|
|
213
205
|
backoffFactor: z$1.number().optional().default(2).describe("multiplier for exponential backoff"),
|
|
214
206
|
bindWithRetry: z$1.boolean().optional().default(true).describe("should we bind with retry"),
|
|
215
|
-
couch:
|
|
207
|
+
couch: CouchUrl.describe("URL of the couch db without embedded credentials"),
|
|
216
208
|
initialDelay: z$1.number().optional().default(1e3).describe("initial retry delay in milliseconds"),
|
|
217
209
|
logger: LoggerSchema.optional().describe("logging interface supporting winston-like or simple function interface"),
|
|
218
210
|
maxRetries: z$1.number().optional().default(3).describe("maximum number of retry attempts"),
|
|
219
|
-
|
|
220
|
-
throwOnGetNotFound: z$1.boolean().optional().default(false).describe("if
|
|
211
|
+
request: RequestOptions.optional().describe("default request controls for CouchDB requests"),
|
|
212
|
+
throwOnGetNotFound: z$1.boolean().optional().default(false).describe("if true, get() throws NotFoundError on 404; otherwise it returns null"),
|
|
221
213
|
useConsoleLogger: z$1.boolean().optional().default(false).describe("turn on console as a fallback logger"),
|
|
222
214
|
"~emitter": z$1.any().optional().describe("emitter for events"),
|
|
223
215
|
"~normalizedLogger": z$1.any().optional()
|
|
224
216
|
}).describe("The std config object");
|
|
225
217
|
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region impl/utils/response.mts
|
|
220
|
+
const isRecord = (value) => {
|
|
221
|
+
return value !== null && typeof value === "object";
|
|
222
|
+
};
|
|
223
|
+
const SUCCESS_STATUS_CODES = {
|
|
224
|
+
bulkGet: [200],
|
|
225
|
+
bulkSave: [201, 202],
|
|
226
|
+
changesFeed: [200],
|
|
227
|
+
database: [200],
|
|
228
|
+
documentDelete: [200, 202],
|
|
229
|
+
documentRead: [200],
|
|
230
|
+
documentWrite: [
|
|
231
|
+
200,
|
|
232
|
+
201,
|
|
233
|
+
202
|
|
234
|
+
],
|
|
235
|
+
viewQuery: [200],
|
|
236
|
+
viewStream: [200]
|
|
237
|
+
};
|
|
238
|
+
const getCouchError = (value) => {
|
|
239
|
+
if (!isRecord(value) || typeof value.error !== "string") return;
|
|
240
|
+
return value.error;
|
|
241
|
+
};
|
|
242
|
+
const isSuccessStatusCode = (profile, statusCode) => {
|
|
243
|
+
return SUCCESS_STATUS_CODES[profile].includes(statusCode);
|
|
244
|
+
};
|
|
245
|
+
|
|
226
246
|
//#endregion
|
|
227
247
|
//#region impl/utils/errors.mts
|
|
228
248
|
const RETRYABLE_STATUS_CODES = new Set([
|
|
@@ -248,6 +268,35 @@ const isNetworkError = (value) => {
|
|
|
248
268
|
const candidate = value;
|
|
249
269
|
return typeof candidate.code === "string" && candidate.code in NETWORK_ERROR_STATUS_MAP;
|
|
250
270
|
};
|
|
271
|
+
const getNestedNetworkError = (value) => {
|
|
272
|
+
if (isNetworkError(value)) return value;
|
|
273
|
+
if (typeof value !== "object" || value === null) return null;
|
|
274
|
+
const candidate = value;
|
|
275
|
+
return isNetworkError(candidate.cause) ? candidate.cause : null;
|
|
276
|
+
};
|
|
277
|
+
/**
|
|
278
|
+
* Shared base class for operational errors thrown by hide-a-bed.
|
|
279
|
+
*
|
|
280
|
+
* @public
|
|
281
|
+
*/
|
|
282
|
+
var HideABedError = class extends Error {
|
|
283
|
+
category;
|
|
284
|
+
couchError;
|
|
285
|
+
docId;
|
|
286
|
+
operation;
|
|
287
|
+
retryable;
|
|
288
|
+
statusCode;
|
|
289
|
+
constructor(message, options) {
|
|
290
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
291
|
+
this.name = "HideABedError";
|
|
292
|
+
this.category = options.category;
|
|
293
|
+
this.couchError = options.couchError;
|
|
294
|
+
this.docId = options.docId;
|
|
295
|
+
this.operation = options.operation;
|
|
296
|
+
this.retryable = options.retryable;
|
|
297
|
+
this.statusCode = options.statusCode;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
251
300
|
/**
|
|
252
301
|
* Error thrown when a requested CouchDB document cannot be found.
|
|
253
302
|
*
|
|
@@ -257,21 +306,77 @@ const isNetworkError = (value) => {
|
|
|
257
306
|
*
|
|
258
307
|
* @public
|
|
259
308
|
*/
|
|
260
|
-
var NotFoundError = class extends
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
constructor(docId, message = "Document not found") {
|
|
272
|
-
super(message);
|
|
309
|
+
var NotFoundError = class extends HideABedError {
|
|
310
|
+
constructor(docId, options = {}) {
|
|
311
|
+
super(options.message ?? "Document not found", {
|
|
312
|
+
category: "not_found",
|
|
313
|
+
couchError: options.couchError ?? "not_found",
|
|
314
|
+
cause: options.cause,
|
|
315
|
+
docId,
|
|
316
|
+
operation: options.operation,
|
|
317
|
+
retryable: false,
|
|
318
|
+
statusCode: options.statusCode ?? 404
|
|
319
|
+
});
|
|
273
320
|
this.name = "NotFoundError";
|
|
274
|
-
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Error thrown when a single-document mutation conflicts with the current revision.
|
|
325
|
+
*
|
|
326
|
+
* @public
|
|
327
|
+
*/
|
|
328
|
+
var ConflictError = class extends HideABedError {
|
|
329
|
+
constructor(docId, options = {}) {
|
|
330
|
+
super(options.message ?? "Document update conflict", {
|
|
331
|
+
category: "conflict",
|
|
332
|
+
couchError: options.couchError ?? "conflict",
|
|
333
|
+
cause: options.cause,
|
|
334
|
+
docId,
|
|
335
|
+
operation: options.operation,
|
|
336
|
+
retryable: false,
|
|
337
|
+
statusCode: options.statusCode ?? 409
|
|
338
|
+
});
|
|
339
|
+
this.name = "ConflictError";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Error thrown when an operation fails in a non-retryable way.
|
|
344
|
+
*
|
|
345
|
+
* @public
|
|
346
|
+
*/
|
|
347
|
+
var OperationError = class extends HideABedError {
|
|
348
|
+
constructor(message, options = {}) {
|
|
349
|
+
super(message, {
|
|
350
|
+
category: options.category ?? "operation",
|
|
351
|
+
cause: options.cause,
|
|
352
|
+
couchError: options.couchError,
|
|
353
|
+
docId: options.docId,
|
|
354
|
+
operation: options.operation,
|
|
355
|
+
retryable: false,
|
|
356
|
+
statusCode: options.statusCode
|
|
357
|
+
});
|
|
358
|
+
this.name = "OperationError";
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Error thrown when schema validation fails for a document, row, key, or value.
|
|
363
|
+
*
|
|
364
|
+
* @public
|
|
365
|
+
*/
|
|
366
|
+
var ValidationError = class extends HideABedError {
|
|
367
|
+
issues;
|
|
368
|
+
constructor(options) {
|
|
369
|
+
super(options.message ?? "Validation failed", {
|
|
370
|
+
category: "validation",
|
|
371
|
+
cause: options.cause,
|
|
372
|
+
couchError: options.couchError,
|
|
373
|
+
docId: options.docId,
|
|
374
|
+
operation: options.operation,
|
|
375
|
+
retryable: false,
|
|
376
|
+
statusCode: options.statusCode
|
|
377
|
+
});
|
|
378
|
+
this.name = "ValidationError";
|
|
379
|
+
this.issues = options.issues;
|
|
275
380
|
}
|
|
276
381
|
};
|
|
277
382
|
/**
|
|
@@ -283,21 +388,18 @@ var NotFoundError = class extends Error {
|
|
|
283
388
|
*
|
|
284
389
|
* @public
|
|
285
390
|
*/
|
|
286
|
-
var RetryableError = class RetryableError extends
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
constructor(message, statusCode) {
|
|
298
|
-
super(message);
|
|
391
|
+
var RetryableError = class RetryableError extends HideABedError {
|
|
392
|
+
constructor(message, statusCode, options = {}) {
|
|
393
|
+
super(message, {
|
|
394
|
+
category: options.category ?? "retryable",
|
|
395
|
+
cause: options.cause,
|
|
396
|
+
couchError: options.couchError,
|
|
397
|
+
docId: options.docId,
|
|
398
|
+
operation: options.operation,
|
|
399
|
+
retryable: true,
|
|
400
|
+
statusCode
|
|
401
|
+
});
|
|
299
402
|
this.name = "RetryableError";
|
|
300
|
-
this.statusCode = statusCode;
|
|
301
403
|
}
|
|
302
404
|
/**
|
|
303
405
|
* Determines whether the provided status code should be treated as retryable.
|
|
@@ -318,15 +420,45 @@ var RetryableError = class RetryableError extends Error {
|
|
|
318
420
|
* @throws {@link RetryableError} When the error maps to a retryable network condition.
|
|
319
421
|
* @throws {*} Re-throws the original error when it cannot be mapped.
|
|
320
422
|
*/
|
|
321
|
-
static handleNetworkError(err) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
423
|
+
static handleNetworkError(err, operation = "request") {
|
|
424
|
+
const networkError = getNestedNetworkError(err);
|
|
425
|
+
if (networkError) {
|
|
426
|
+
const statusCode = NETWORK_ERROR_STATUS_MAP[networkError.code];
|
|
427
|
+
if (statusCode) throw new RetryableError("Network request failed", statusCode, {
|
|
428
|
+
category: "network",
|
|
429
|
+
cause: err,
|
|
430
|
+
operation
|
|
431
|
+
});
|
|
325
432
|
}
|
|
326
433
|
throw err;
|
|
327
434
|
}
|
|
328
435
|
};
|
|
436
|
+
function createResponseError({ body, defaultMessage, docId, notFoundMessage, operation, statusCode }) {
|
|
437
|
+
const couchError = getCouchError(body);
|
|
438
|
+
if (statusCode === 404 && docId) return new NotFoundError(docId, {
|
|
439
|
+
couchError,
|
|
440
|
+
message: notFoundMessage,
|
|
441
|
+
operation,
|
|
442
|
+
statusCode
|
|
443
|
+
});
|
|
444
|
+
if (statusCode === 409 && docId) return new ConflictError(docId, {
|
|
445
|
+
couchError,
|
|
446
|
+
operation,
|
|
447
|
+
statusCode
|
|
448
|
+
});
|
|
449
|
+
if (RetryableError.isRetryableStatusCode(statusCode)) return new RetryableError(defaultMessage, statusCode, {
|
|
450
|
+
couchError,
|
|
451
|
+
operation
|
|
452
|
+
});
|
|
453
|
+
return new OperationError(defaultMessage, {
|
|
454
|
+
couchError,
|
|
455
|
+
docId,
|
|
456
|
+
operation,
|
|
457
|
+
statusCode
|
|
458
|
+
});
|
|
459
|
+
}
|
|
329
460
|
function isConflictError(err) {
|
|
461
|
+
if (err instanceof ConflictError) return true;
|
|
330
462
|
if (typeof err !== "object" || err === null) return false;
|
|
331
463
|
return err.statusCode === 409;
|
|
332
464
|
}
|
|
@@ -400,27 +532,6 @@ function createLogger(config) {
|
|
|
400
532
|
return normalized;
|
|
401
533
|
}
|
|
402
534
|
|
|
403
|
-
//#endregion
|
|
404
|
-
//#region schema/util.mts
|
|
405
|
-
const MergeNeedleOpts = z$1.function({
|
|
406
|
-
input: [CouchConfig, NeedleBaseOptions],
|
|
407
|
-
output: NeedleOptions
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
//#endregion
|
|
411
|
-
//#region impl/utils/mergeNeedleOpts.mts
|
|
412
|
-
const mergeNeedleOpts = MergeNeedleOpts.implement((config, opts) => {
|
|
413
|
-
if (config.needleOpts) return {
|
|
414
|
-
...opts,
|
|
415
|
-
...config.needleOpts,
|
|
416
|
-
headers: {
|
|
417
|
-
...opts.headers,
|
|
418
|
-
...config.needleOpts.headers ?? {}
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
return opts;
|
|
422
|
-
});
|
|
423
|
-
|
|
424
535
|
//#endregion
|
|
425
536
|
//#region schema/couch/couch.output.schema.ts
|
|
426
537
|
/**
|
|
@@ -493,8 +604,16 @@ const CouchDBInfo = z$1.looseObject({
|
|
|
493
604
|
|
|
494
605
|
//#endregion
|
|
495
606
|
//#region impl/utils/parseRows.mts
|
|
607
|
+
const createValidationError = (issues, options) => {
|
|
608
|
+
return new ValidationError({
|
|
609
|
+
docId: options.docId,
|
|
610
|
+
issues,
|
|
611
|
+
message: options.defaultMessage ?? "Row validation failed",
|
|
612
|
+
operation: options.operation ?? "request"
|
|
613
|
+
});
|
|
614
|
+
};
|
|
496
615
|
async function parseRows(rows, options) {
|
|
497
|
-
if (!Array.isArray(rows)) throw new
|
|
616
|
+
if (!Array.isArray(rows)) throw new OperationError(options.defaultMessage ?? "Request failed", { operation: options.operation ?? "request" });
|
|
498
617
|
const isFinalRow = (row) => row !== "skip";
|
|
499
618
|
return (await Promise.all(rows.map(async (row) => {
|
|
500
619
|
try {
|
|
@@ -506,12 +625,20 @@ async function parseRows(rows, options) {
|
|
|
506
625
|
const parsedRow = z$1.looseObject(ViewRow.shape).parse(row);
|
|
507
626
|
if (options.keySchema) {
|
|
508
627
|
const parsedKey$1 = await options.keySchema["~standard"].validate(row.key);
|
|
509
|
-
if (parsedKey$1.issues) throw parsedKey$1.issues
|
|
628
|
+
if (parsedKey$1.issues) throw createValidationError(parsedKey$1.issues, {
|
|
629
|
+
defaultMessage: options.defaultMessage,
|
|
630
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
631
|
+
operation: options.operation
|
|
632
|
+
});
|
|
510
633
|
parsedRow.key = parsedKey$1.value;
|
|
511
634
|
}
|
|
512
635
|
if (options.valueSchema) {
|
|
513
636
|
const parsedValue$1 = await options.valueSchema["~standard"].validate(row.value);
|
|
514
|
-
if (parsedValue$1.issues) throw parsedValue$1.issues
|
|
637
|
+
if (parsedValue$1.issues) throw createValidationError(parsedValue$1.issues, {
|
|
638
|
+
defaultMessage: options.defaultMessage,
|
|
639
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
640
|
+
operation: options.operation
|
|
641
|
+
});
|
|
515
642
|
parsedRow.value = parsedValue$1.value;
|
|
516
643
|
}
|
|
517
644
|
return parsedRow;
|
|
@@ -522,17 +649,29 @@ async function parseRows(rows, options) {
|
|
|
522
649
|
if (options.docSchema) {
|
|
523
650
|
const parsedDocRes = await options.docSchema["~standard"].validate(row.doc);
|
|
524
651
|
if (parsedDocRes.issues) if (options.onInvalidDoc === "skip") return "skip";
|
|
525
|
-
else throw parsedDocRes.issues
|
|
652
|
+
else throw createValidationError(parsedDocRes.issues, {
|
|
653
|
+
defaultMessage: options.defaultMessage,
|
|
654
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
655
|
+
operation: options.operation
|
|
656
|
+
});
|
|
526
657
|
else parsedDoc = parsedDocRes.value;
|
|
527
658
|
}
|
|
528
659
|
if (options.keySchema) {
|
|
529
660
|
const parsedKeyRes = await options.keySchema["~standard"].validate(row.key);
|
|
530
|
-
if (parsedKeyRes.issues) throw parsedKeyRes.issues
|
|
661
|
+
if (parsedKeyRes.issues) throw createValidationError(parsedKeyRes.issues, {
|
|
662
|
+
defaultMessage: options.defaultMessage,
|
|
663
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
664
|
+
operation: options.operation
|
|
665
|
+
});
|
|
531
666
|
else parsedKey = parsedKeyRes.value;
|
|
532
667
|
}
|
|
533
668
|
if (options.valueSchema) {
|
|
534
669
|
const parsedValueRes = await options.valueSchema["~standard"].validate(row.value);
|
|
535
|
-
if (parsedValueRes.issues) throw parsedValueRes.issues
|
|
670
|
+
if (parsedValueRes.issues) throw createValidationError(parsedValueRes.issues, {
|
|
671
|
+
defaultMessage: options.defaultMessage,
|
|
672
|
+
docId: typeof row.id === "string" ? row.id : void 0,
|
|
673
|
+
operation: options.operation
|
|
674
|
+
});
|
|
536
675
|
else parsedValue = parsedValueRes.value;
|
|
537
676
|
}
|
|
538
677
|
return {
|
|
@@ -548,6 +687,137 @@ async function parseRows(rows, options) {
|
|
|
548
687
|
}))).filter(isFinalRow);
|
|
549
688
|
}
|
|
550
689
|
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region impl/utils/request.mts
|
|
692
|
+
const definedSignals = (signals) => {
|
|
693
|
+
return signals.filter((signal) => signal != null);
|
|
694
|
+
};
|
|
695
|
+
const composeAbortSignal = (internalSignal, request) => {
|
|
696
|
+
const timeoutSignal = typeof request?.timeout === "number" ? AbortSignal.timeout(request.timeout) : void 0;
|
|
697
|
+
const signals = definedSignals([
|
|
698
|
+
internalSignal,
|
|
699
|
+
request?.signal,
|
|
700
|
+
timeoutSignal
|
|
701
|
+
]);
|
|
702
|
+
return {
|
|
703
|
+
signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
|
|
704
|
+
timedOut: () => timeoutSignal?.aborted === true
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region impl/utils/fetch.mts
|
|
710
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
711
|
+
const hasHeader = (headers, name) => {
|
|
712
|
+
const expected = name.toLowerCase();
|
|
713
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === expected);
|
|
714
|
+
};
|
|
715
|
+
const toBasicAuthHeader = ({ username, password }) => {
|
|
716
|
+
return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
|
717
|
+
};
|
|
718
|
+
const prepareRequest = (options) => {
|
|
719
|
+
const auth = options.auth;
|
|
720
|
+
const headers = { ...options.headers ?? {} };
|
|
721
|
+
if (auth && !hasHeader(headers, "Authorization")) headers.Authorization = toBasicAuthHeader(auth);
|
|
722
|
+
return { headers };
|
|
723
|
+
};
|
|
724
|
+
const isAbortError = (err) => {
|
|
725
|
+
return err instanceof DOMException && err.name === "AbortError";
|
|
726
|
+
};
|
|
727
|
+
const isTimeoutError = (err) => {
|
|
728
|
+
return err instanceof DOMException && err.name === "TimeoutError";
|
|
729
|
+
};
|
|
730
|
+
const encodeBody = (body) => {
|
|
731
|
+
if (body == null) return void 0;
|
|
732
|
+
if (typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof Blob || body instanceof FormData || body instanceof URLSearchParams || body instanceof ReadableStream) return body;
|
|
733
|
+
return JSON.stringify(body);
|
|
734
|
+
};
|
|
735
|
+
const parseJsonResponse = async (response) => {
|
|
736
|
+
if (response.status === 204 || response.status === 205) return null;
|
|
737
|
+
const text = await response.text();
|
|
738
|
+
if (text.trim() === "") return null;
|
|
739
|
+
try {
|
|
740
|
+
return JSON.parse(text);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
if (response.ok) throw err;
|
|
743
|
+
return text;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
async function fetchCouchJson(options) {
|
|
747
|
+
let response;
|
|
748
|
+
const { headers } = prepareRequest(options);
|
|
749
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request);
|
|
750
|
+
try {
|
|
751
|
+
response = await fetch(options.url, {
|
|
752
|
+
method: options.method,
|
|
753
|
+
headers: {
|
|
754
|
+
...JSON_HEADERS,
|
|
755
|
+
...headers
|
|
756
|
+
},
|
|
757
|
+
body: encodeBody(options.body),
|
|
758
|
+
signal,
|
|
759
|
+
dispatcher: options.request?.dispatcher
|
|
760
|
+
});
|
|
761
|
+
} catch (err) {
|
|
762
|
+
if (timedOut() || isTimeoutError(err)) throw new RetryableError("Request timed out", 503, {
|
|
763
|
+
category: "network",
|
|
764
|
+
cause: err,
|
|
765
|
+
operation: options.operation
|
|
766
|
+
});
|
|
767
|
+
if (isAbortError(err)) throw err;
|
|
768
|
+
RetryableError.handleNetworkError(err, options.operation);
|
|
769
|
+
}
|
|
770
|
+
return {
|
|
771
|
+
body: await parseJsonResponse(response),
|
|
772
|
+
headers: response.headers,
|
|
773
|
+
statusCode: response.status
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async function fetchCouchStream(options) {
|
|
777
|
+
let response;
|
|
778
|
+
const { headers } = prepareRequest(options);
|
|
779
|
+
const { signal, timedOut } = composeAbortSignal(options.signal, options.request);
|
|
780
|
+
try {
|
|
781
|
+
response = await fetch(options.url, {
|
|
782
|
+
method: options.method,
|
|
783
|
+
headers,
|
|
784
|
+
body: encodeBody(options.body),
|
|
785
|
+
signal,
|
|
786
|
+
dispatcher: options.request?.dispatcher
|
|
787
|
+
});
|
|
788
|
+
} catch (err) {
|
|
789
|
+
if (timedOut() || isTimeoutError(err)) throw new RetryableError("Request timed out", 503, {
|
|
790
|
+
category: "network",
|
|
791
|
+
cause: err,
|
|
792
|
+
operation: options.operation
|
|
793
|
+
});
|
|
794
|
+
if (isAbortError(err)) throw err;
|
|
795
|
+
RetryableError.handleNetworkError(err, options.operation);
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
body: response.body,
|
|
799
|
+
headers: response.headers,
|
|
800
|
+
statusCode: response.status
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
//#endregion
|
|
805
|
+
//#region impl/utils/url.mts
|
|
806
|
+
const ensureDirectoryUrl = (value) => {
|
|
807
|
+
const url = new URL(value);
|
|
808
|
+
if (!url.pathname.endsWith("/")) url.pathname = `${url.pathname}/`;
|
|
809
|
+
return url;
|
|
810
|
+
};
|
|
811
|
+
const createCouchDbUrl = (value) => {
|
|
812
|
+
return new URL(value);
|
|
813
|
+
};
|
|
814
|
+
const createCouchPathUrl = (path, base) => {
|
|
815
|
+
return new URL(path, ensureDirectoryUrl(base));
|
|
816
|
+
};
|
|
817
|
+
const createCouchDocUrl = (docId, base) => {
|
|
818
|
+
return new URL(encodeURIComponent(docId), ensureDirectoryUrl(base));
|
|
819
|
+
};
|
|
820
|
+
|
|
551
821
|
//#endregion
|
|
552
822
|
//#region impl/bulkGet.mts
|
|
553
823
|
/**
|
|
@@ -560,7 +830,7 @@ async function parseRows(rows, options) {
|
|
|
560
830
|
* @returns The raw response body from CouchDB
|
|
561
831
|
*
|
|
562
832
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
563
|
-
* @throws {
|
|
833
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
564
834
|
*/
|
|
565
835
|
async function executeBulkGet(_config, ids, includeDocs) {
|
|
566
836
|
const configParseResult = CouchConfig.safeParse(_config);
|
|
@@ -571,26 +841,35 @@ async function executeBulkGet(_config, ids, includeDocs) {
|
|
|
571
841
|
throw configParseResult.error;
|
|
572
842
|
}
|
|
573
843
|
const config = configParseResult.data;
|
|
574
|
-
const url =
|
|
844
|
+
const url = createCouchPathUrl("_all_docs", config.couch);
|
|
845
|
+
if (includeDocs) url.searchParams.append("include_docs", "true");
|
|
575
846
|
const payload = { keys: ids };
|
|
576
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
577
|
-
json: true,
|
|
578
|
-
headers: { "Content-Type": "application/json" }
|
|
579
|
-
});
|
|
580
847
|
try {
|
|
581
|
-
const resp = await
|
|
848
|
+
const resp = await fetchCouchJson({
|
|
849
|
+
auth: config.auth,
|
|
850
|
+
method: "POST",
|
|
851
|
+
operation: "request",
|
|
852
|
+
request: config.request,
|
|
853
|
+
url,
|
|
854
|
+
body: payload
|
|
855
|
+
});
|
|
582
856
|
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
583
857
|
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
584
|
-
throw new RetryableError("
|
|
858
|
+
throw new RetryableError("Bulk get failed", resp.statusCode, { operation: "request" });
|
|
585
859
|
}
|
|
586
|
-
if (resp.statusCode
|
|
860
|
+
if (!isSuccessStatusCode("bulkGet", resp.statusCode)) {
|
|
587
861
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
588
|
-
throw
|
|
862
|
+
throw createResponseError({
|
|
863
|
+
body: resp.body,
|
|
864
|
+
defaultMessage: "Bulk get failed",
|
|
865
|
+
operation: "request",
|
|
866
|
+
statusCode: resp.statusCode
|
|
867
|
+
});
|
|
589
868
|
}
|
|
590
869
|
return resp.body;
|
|
591
870
|
} catch (err) {
|
|
592
871
|
logger.error("Network error during bulk get:", err);
|
|
593
|
-
RetryableError.handleNetworkError(err);
|
|
872
|
+
RetryableError.handleNetworkError(err, "request");
|
|
594
873
|
}
|
|
595
874
|
}
|
|
596
875
|
/**
|
|
@@ -605,16 +884,22 @@ async function executeBulkGet(_config, ids, includeDocs) {
|
|
|
605
884
|
* @returns The bulk get response with rows optionally validated against the supplied document schema.
|
|
606
885
|
*
|
|
607
886
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
608
|
-
* @throws {
|
|
609
|
-
* @throws {
|
|
887
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
888
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
610
889
|
*/
|
|
611
890
|
async function _bulkGetWithOptions(config, ids, options = {}) {
|
|
612
891
|
const body = await executeBulkGet(config, ids, options.includeDocs ?? true);
|
|
613
|
-
if (!body) throw new RetryableError("
|
|
614
|
-
if (body.error) throw
|
|
892
|
+
if (!body) throw new RetryableError("Bulk get failed", 503, { operation: "request" });
|
|
893
|
+
if (body.error) throw createResponseError({
|
|
894
|
+
body,
|
|
895
|
+
defaultMessage: "Bulk get failed",
|
|
896
|
+
operation: "request"
|
|
897
|
+
});
|
|
615
898
|
const docSchema = options.validate?.docSchema || CouchDoc;
|
|
616
899
|
const rows = await parseRows(body.rows, {
|
|
900
|
+
defaultMessage: "Bulk get failed",
|
|
617
901
|
onInvalidDoc: options.validate?.onInvalidDoc,
|
|
902
|
+
operation: "request",
|
|
618
903
|
docSchema
|
|
619
904
|
});
|
|
620
905
|
return {
|
|
@@ -640,8 +925,8 @@ async function _bulkGetWithOptions(config, ids, options = {}) {
|
|
|
640
925
|
* @returns The bulk get response with rows optionally validated against the supplied document schema.
|
|
641
926
|
*
|
|
642
927
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
643
|
-
* @throws {
|
|
644
|
-
* @throws {
|
|
928
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
929
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
645
930
|
*/
|
|
646
931
|
async function bulkGet(config, ids, options = {}) {
|
|
647
932
|
return _bulkGetWithOptions(config, ids, {
|
|
@@ -652,7 +937,7 @@ async function bulkGet(config, ids, options = {}) {
|
|
|
652
937
|
/**
|
|
653
938
|
* Bulk get documents by IDs and return a dictionary of found and not found documents.
|
|
654
939
|
*
|
|
655
|
-
* @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
|
|
940
|
+
* @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.
|
|
656
941
|
*
|
|
657
942
|
* @param config - CouchDB configuration data that is validated before use.
|
|
658
943
|
* @param ids - Array of document IDs to retrieve.
|
|
@@ -661,8 +946,8 @@ async function bulkGet(config, ids, options = {}) {
|
|
|
661
946
|
* @returns An object containing found documents and not found rows.
|
|
662
947
|
*
|
|
663
948
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
664
|
-
* @throws {
|
|
665
|
-
* @throws {
|
|
949
|
+
* @throws {ValidationError} When returned documents fail schema validation.
|
|
950
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
666
951
|
*/
|
|
667
952
|
async function bulkGetDictionary(config, ids, options) {
|
|
668
953
|
const response = await bulkGet(config, ids, {
|
|
@@ -693,60 +978,65 @@ async function bulkGetDictionary(config, ids, options) {
|
|
|
693
978
|
|
|
694
979
|
//#endregion
|
|
695
980
|
//#region impl/get.mts
|
|
696
|
-
const ValidSchema = z$1.custom((value) => {
|
|
981
|
+
const ValidSchema$1 = z$1.custom((value) => {
|
|
697
982
|
return value !== null && typeof value === "object" && "~standard" in value;
|
|
698
983
|
}, { message: "docSchema must be a valid StandardSchemaV1 schema" });
|
|
699
|
-
const CouchGetOptions = z$1.
|
|
984
|
+
const CouchGetOptions = z$1.strictObject({
|
|
700
985
|
rev: z$1.string().optional().describe("the couch doc revision"),
|
|
701
|
-
validate: z$1.object({ docSchema: ValidSchema.optional() }).optional().describe("optional document validation rules")
|
|
986
|
+
validate: z$1.object({ docSchema: ValidSchema$1.optional() }).optional().describe("optional document validation rules")
|
|
702
987
|
});
|
|
703
|
-
async function _getWithOptions(
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
validate: options.validate
|
|
707
|
-
});
|
|
988
|
+
async function _getWithOptions(configInput, id, options) {
|
|
989
|
+
const config = CouchConfig.parse(configInput);
|
|
990
|
+
const parsedOptions = CouchGetOptions.parse(options);
|
|
708
991
|
const logger = createLogger(config);
|
|
709
992
|
const rev = parsedOptions.rev;
|
|
710
|
-
const
|
|
711
|
-
const url =
|
|
712
|
-
|
|
713
|
-
json: true,
|
|
714
|
-
headers: { "Content-Type": "application/json" }
|
|
715
|
-
});
|
|
993
|
+
const operation = rev ? "getAtRev" : "get";
|
|
994
|
+
const url = createCouchDocUrl(id, config.couch);
|
|
995
|
+
if (rev) url.searchParams.set("rev", rev);
|
|
716
996
|
logger.info(`Getting document with id: ${id}, rev ${rev ?? "latest"}`);
|
|
717
997
|
try {
|
|
718
|
-
const resp = await
|
|
998
|
+
const resp = await fetchCouchJson({
|
|
999
|
+
auth: config.auth,
|
|
1000
|
+
method: "GET",
|
|
1001
|
+
operation,
|
|
1002
|
+
request: config.request,
|
|
1003
|
+
url
|
|
1004
|
+
});
|
|
719
1005
|
if (!resp) {
|
|
720
1006
|
logger.error("No response received from get request");
|
|
721
|
-
throw new RetryableError("
|
|
1007
|
+
throw new RetryableError("Request failed", 503, { operation });
|
|
722
1008
|
}
|
|
723
1009
|
const body = resp.body ?? null;
|
|
724
1010
|
if (resp.statusCode === 404) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
return null;
|
|
1011
|
+
logger.warn(`Document not found: ${id}, rev ${rev ?? "latest"}`);
|
|
1012
|
+
if (config.throwOnGetNotFound === false) return null;
|
|
1013
|
+
throw new NotFoundError(id, {
|
|
1014
|
+
operation,
|
|
1015
|
+
statusCode: resp.statusCode
|
|
1016
|
+
});
|
|
732
1017
|
}
|
|
733
|
-
if (
|
|
734
|
-
const reason = typeof body?.reason === "string" ? body.reason : "retryable error";
|
|
735
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
736
|
-
throw new RetryableError(reason, resp.statusCode);
|
|
737
|
-
}
|
|
738
|
-
if (resp.statusCode !== 200) {
|
|
739
|
-
const reason = typeof body?.reason === "string" ? body.reason : "failed";
|
|
1018
|
+
if (!isSuccessStatusCode("documentRead", resp.statusCode)) {
|
|
740
1019
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
741
|
-
throw
|
|
1020
|
+
throw createResponseError({
|
|
1021
|
+
body,
|
|
1022
|
+
defaultMessage: "Failed to fetch document",
|
|
1023
|
+
docId: id,
|
|
1024
|
+
operation,
|
|
1025
|
+
statusCode: resp.statusCode
|
|
1026
|
+
});
|
|
742
1027
|
}
|
|
743
1028
|
const typedDoc = await (parsedOptions.validate?.docSchema ?? CouchDoc)["~standard"].validate(body);
|
|
744
|
-
if (typedDoc.issues) throw
|
|
1029
|
+
if (typedDoc.issues) throw new ValidationError({
|
|
1030
|
+
docId: id,
|
|
1031
|
+
issues: typedDoc.issues,
|
|
1032
|
+
message: "Document validation failed",
|
|
1033
|
+
operation
|
|
1034
|
+
});
|
|
745
1035
|
logger.info(`Successfully retrieved document: ${id}, rev ${rev ?? "latest"}`);
|
|
746
1036
|
return typedDoc.value;
|
|
747
1037
|
} catch (err) {
|
|
748
1038
|
logger.error("Error during get operation:", err);
|
|
749
|
-
RetryableError.handleNetworkError(err);
|
|
1039
|
+
RetryableError.handleNetworkError(err, operation);
|
|
750
1040
|
}
|
|
751
1041
|
}
|
|
752
1042
|
async function get(config, id, options) {
|
|
@@ -834,89 +1124,137 @@ function queryString(options = {}) {
|
|
|
834
1124
|
*/
|
|
835
1125
|
async function queryStream(rawConfig, view, options, onRow) {
|
|
836
1126
|
return new Promise((resolve, reject) => {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
parse_response: false
|
|
860
|
-
});
|
|
861
|
-
const parserPipeline = Chain.chain([
|
|
862
|
-
new Parser(),
|
|
863
|
-
new Pick({ filter: "rows" }),
|
|
864
|
-
new StreamArray()
|
|
865
|
-
]);
|
|
866
|
-
let rowCount = 0;
|
|
867
|
-
let settled = false;
|
|
868
|
-
const settleReject = (err) => {
|
|
869
|
-
if (settled) return;
|
|
870
|
-
settled = true;
|
|
871
|
-
reject(err);
|
|
872
|
-
};
|
|
873
|
-
const settleResolve = () => {
|
|
874
|
-
if (settled) return;
|
|
875
|
-
settled = true;
|
|
876
|
-
resolve();
|
|
877
|
-
};
|
|
878
|
-
let request = null;
|
|
879
|
-
parserPipeline.on("data", (chunk) => {
|
|
880
|
-
try {
|
|
881
|
-
rowCount++;
|
|
882
|
-
onRow(chunk.value);
|
|
883
|
-
} catch (callbackErr) {
|
|
884
|
-
const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr));
|
|
885
|
-
parserPipeline.destroy(error);
|
|
886
|
-
settleReject(error);
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
parserPipeline.on("error", (err) => {
|
|
890
|
-
logger.error("Stream parsing error:", err);
|
|
891
|
-
parserPipeline.destroy();
|
|
892
|
-
settleReject(new Error(`Stream parsing error: ${err.message}`, { cause: err }));
|
|
893
|
-
});
|
|
894
|
-
parserPipeline.on("end", () => {
|
|
895
|
-
logger.info(`Stream completed, processed ${rowCount} rows`);
|
|
896
|
-
settleResolve();
|
|
897
|
-
});
|
|
898
|
-
request = method === "GET" ? needle.get(url, mergedOpts) : needle.post(url, payload, mergedOpts);
|
|
899
|
-
request.on("response", (response) => {
|
|
900
|
-
logger.debug(`Received response with status code: ${response.statusCode}`);
|
|
901
|
-
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
902
|
-
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
903
|
-
settleReject(new RetryableError("retryable error during stream query", response.statusCode));
|
|
904
|
-
request.destroy();
|
|
1127
|
+
(async () => {
|
|
1128
|
+
const config = CouchConfig.parse(rawConfig);
|
|
1129
|
+
const logger = createLogger(config);
|
|
1130
|
+
logger.info(`Starting view query stream: ${view}`);
|
|
1131
|
+
const queryOptions = ViewOptions.parse(options ?? {});
|
|
1132
|
+
const request = config.request;
|
|
1133
|
+
logger.debug("Query options:", {
|
|
1134
|
+
...queryOptions,
|
|
1135
|
+
request
|
|
1136
|
+
});
|
|
1137
|
+
let method = "GET";
|
|
1138
|
+
let payload = null;
|
|
1139
|
+
let qs = queryString(queryOptions);
|
|
1140
|
+
logger.debug("Generated query string:", qs);
|
|
1141
|
+
if (typeof queryOptions.keys !== "undefined") {
|
|
1142
|
+
const MAX_URL_LENGTH = 2e3;
|
|
1143
|
+
const keysAsString = `keys=${encodeURIComponent(JSON.stringify(queryOptions.keys))}`;
|
|
1144
|
+
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) qs += (qs.length > 0 ? "&" : "") + keysAsString;
|
|
1145
|
+
else {
|
|
1146
|
+
method = "POST";
|
|
1147
|
+
payload = { keys: queryOptions.keys };
|
|
1148
|
+
}
|
|
905
1149
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1150
|
+
const url = createCouchPathUrl(view, config.couch);
|
|
1151
|
+
if (qs) url.search = qs;
|
|
1152
|
+
const requestHeaders = { "Content-Type": "application/json" };
|
|
1153
|
+
const abortController = new AbortController();
|
|
1154
|
+
const requestAbortHandler = () => {
|
|
1155
|
+
const reason = request?.signal?.reason instanceof Error ? request.signal.reason : new DOMException("The operation was aborted.", "AbortError");
|
|
1156
|
+
abortController.abort(reason);
|
|
1157
|
+
responseStream?.destroy(reason);
|
|
1158
|
+
parserPipeline.destroy(reason);
|
|
1159
|
+
settleReject(reason);
|
|
1160
|
+
};
|
|
1161
|
+
const parserPipeline = Chain.chain([
|
|
1162
|
+
new Parser(),
|
|
1163
|
+
new Pick({ filter: "rows" }),
|
|
1164
|
+
new StreamArray()
|
|
1165
|
+
]);
|
|
1166
|
+
let rowCount = 0;
|
|
1167
|
+
let settled = false;
|
|
1168
|
+
const settleReject = (err) => {
|
|
1169
|
+
if (settled) return;
|
|
1170
|
+
settled = true;
|
|
1171
|
+
request?.signal?.removeEventListener("abort", requestAbortHandler);
|
|
1172
|
+
reject(err);
|
|
1173
|
+
};
|
|
1174
|
+
const settleResolve = () => {
|
|
1175
|
+
if (settled) return;
|
|
1176
|
+
settled = true;
|
|
1177
|
+
request?.signal?.removeEventListener("abort", requestAbortHandler);
|
|
1178
|
+
resolve();
|
|
1179
|
+
};
|
|
1180
|
+
let responseStream = null;
|
|
1181
|
+
request?.signal?.addEventListener("abort", requestAbortHandler, { once: true });
|
|
1182
|
+
parserPipeline.on("data", (chunk) => {
|
|
1183
|
+
try {
|
|
1184
|
+
rowCount++;
|
|
1185
|
+
onRow(chunk.value);
|
|
1186
|
+
} catch (callbackErr) {
|
|
1187
|
+
const error = callbackErr instanceof Error ? callbackErr : new Error(String(callbackErr));
|
|
1188
|
+
parserPipeline.destroy(error);
|
|
1189
|
+
settleReject(error);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
parserPipeline.on("error", (err) => {
|
|
1193
|
+
logger.error("Stream parsing error:", err);
|
|
1194
|
+
parserPipeline.destroy();
|
|
1195
|
+
settleReject(new OperationError("Stream parsing failed", {
|
|
1196
|
+
cause: err,
|
|
1197
|
+
operation: "queryStream"
|
|
1198
|
+
}));
|
|
1199
|
+
});
|
|
1200
|
+
parserPipeline.on("end", () => {
|
|
1201
|
+
logger.info(`Stream completed, processed ${rowCount} rows`);
|
|
1202
|
+
settleResolve();
|
|
1203
|
+
});
|
|
910
1204
|
try {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1205
|
+
const response = await fetchCouchStream({
|
|
1206
|
+
auth: config.auth,
|
|
1207
|
+
method,
|
|
1208
|
+
operation: "queryStream",
|
|
1209
|
+
url,
|
|
1210
|
+
body: method === "POST" ? payload : void 0,
|
|
1211
|
+
headers: requestHeaders,
|
|
1212
|
+
request,
|
|
1213
|
+
signal: abortController.signal
|
|
1214
|
+
});
|
|
1215
|
+
logger.debug(`Received response with status code: ${response.statusCode}`);
|
|
1216
|
+
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
1217
|
+
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
1218
|
+
abortController.abort();
|
|
1219
|
+
settleReject(new RetryableError("Stream query failed", response.statusCode, { operation: "queryStream" }));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (!isSuccessStatusCode("viewStream", response.statusCode)) {
|
|
1223
|
+
abortController.abort();
|
|
1224
|
+
settleReject(createResponseError({
|
|
1225
|
+
defaultMessage: "Stream query failed",
|
|
1226
|
+
operation: "queryStream",
|
|
1227
|
+
statusCode: response.statusCode
|
|
1228
|
+
}));
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (!response.body) {
|
|
1232
|
+
settleReject(new RetryableError("Stream query failed", 503, { operation: "queryStream" }));
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
responseStream = Readable.fromWeb(response.body);
|
|
1236
|
+
responseStream.on("error", (err) => {
|
|
1237
|
+
logger.error("Network error during stream query:", err);
|
|
1238
|
+
parserPipeline.destroy(err);
|
|
1239
|
+
try {
|
|
1240
|
+
RetryableError.handleNetworkError(err, "queryStream");
|
|
1241
|
+
} catch (retryErr) {
|
|
1242
|
+
settleReject(retryErr);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
responseStream.pipe(parserPipeline);
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
logger.error("Network error during stream query:", err);
|
|
1249
|
+
parserPipeline.destroy(err);
|
|
1250
|
+
try {
|
|
1251
|
+
RetryableError.handleNetworkError(err, "queryStream");
|
|
1252
|
+
} catch (retryErr) {
|
|
1253
|
+
settleReject(retryErr);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
917
1256
|
}
|
|
918
|
-
});
|
|
919
|
-
request.pipe(parserPipeline);
|
|
1257
|
+
})();
|
|
920
1258
|
});
|
|
921
1259
|
}
|
|
922
1260
|
|
|
@@ -925,35 +1263,46 @@ async function queryStream(rawConfig, view, options, onRow) {
|
|
|
925
1263
|
const put = async (configInput, doc) => {
|
|
926
1264
|
const config = CouchConfig.parse(configInput);
|
|
927
1265
|
const logger = createLogger(config);
|
|
928
|
-
const url =
|
|
1266
|
+
const url = createCouchDocUrl(doc._id, config.couch);
|
|
929
1267
|
const body = doc;
|
|
930
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
931
|
-
json: true,
|
|
932
|
-
headers: { "Content-Type": "application/json" }
|
|
933
|
-
});
|
|
934
1268
|
logger.info(`Putting document with id: ${doc._id}`);
|
|
935
1269
|
let resp;
|
|
936
1270
|
try {
|
|
937
|
-
resp = await
|
|
1271
|
+
resp = await fetchCouchJson({
|
|
1272
|
+
auth: config.auth,
|
|
1273
|
+
method: "PUT",
|
|
1274
|
+
operation: "put",
|
|
1275
|
+
request: config.request,
|
|
1276
|
+
url,
|
|
1277
|
+
body
|
|
1278
|
+
});
|
|
938
1279
|
} catch (err) {
|
|
939
1280
|
logger.error("Error during put operation:", err);
|
|
940
|
-
RetryableError.handleNetworkError(err);
|
|
1281
|
+
RetryableError.handleNetworkError(err, "put");
|
|
941
1282
|
}
|
|
942
1283
|
if (!resp) {
|
|
943
1284
|
logger.error("No response received from put request");
|
|
944
|
-
throw new RetryableError("
|
|
1285
|
+
throw new RetryableError("Put failed", 503, { operation: "put" });
|
|
945
1286
|
}
|
|
946
|
-
const result = resp
|
|
1287
|
+
const result = { ...isRecord(resp.body) ? resp.body : {} };
|
|
947
1288
|
result.statusCode = resp.statusCode;
|
|
948
1289
|
if (resp.statusCode === 409) {
|
|
949
1290
|
logger.warn(`Conflict detected for document: ${doc._id}`);
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1291
|
+
throw new ConflictError(doc._id, {
|
|
1292
|
+
couchError: typeof result.error === "string" ? result.error : void 0,
|
|
1293
|
+
operation: "put",
|
|
1294
|
+
statusCode: resp.statusCode
|
|
1295
|
+
});
|
|
953
1296
|
}
|
|
954
|
-
if (
|
|
955
|
-
logger.
|
|
956
|
-
throw
|
|
1297
|
+
if (!isSuccessStatusCode("documentWrite", resp.statusCode) || !result.ok) {
|
|
1298
|
+
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1299
|
+
throw createResponseError({
|
|
1300
|
+
body: resp.body,
|
|
1301
|
+
defaultMessage: "Put failed",
|
|
1302
|
+
docId: doc._id,
|
|
1303
|
+
operation: "put",
|
|
1304
|
+
statusCode: resp.statusCode
|
|
1305
|
+
});
|
|
957
1306
|
}
|
|
958
1307
|
logger.info(`Successfully saved document: ${doc._id}`);
|
|
959
1308
|
return CouchPutResponse.parse(result);
|
|
@@ -971,7 +1320,10 @@ const PatchProperties = z$1.looseObject({ _rev: z$1.string("_rev is required for
|
|
|
971
1320
|
* @param _properties - Properties to merge into the document (must include _rev)
|
|
972
1321
|
* @returns The result of the put operation
|
|
973
1322
|
*
|
|
974
|
-
* @throws
|
|
1323
|
+
* @throws {ConflictError} When the supplied `_rev` does not match the current document revision.
|
|
1324
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
1325
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs while reading or saving.
|
|
1326
|
+
* @throws {OperationError} When a non-retryable operational failure occurs.
|
|
975
1327
|
*/
|
|
976
1328
|
const patch = async (configInput, id, _properties) => {
|
|
977
1329
|
const config = CouchConfig.parse(configInput);
|
|
@@ -979,12 +1331,15 @@ const patch = async (configInput, id, _properties) => {
|
|
|
979
1331
|
const logger = createLogger(configInput);
|
|
980
1332
|
logger.info(`Starting patch operation for document ${id}`);
|
|
981
1333
|
logger.debug("Patch properties:", properties);
|
|
982
|
-
const doc = await get(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1334
|
+
const doc = await get({
|
|
1335
|
+
...config,
|
|
1336
|
+
throwOnGetNotFound: true
|
|
1337
|
+
}, id);
|
|
1338
|
+
if (!doc) throw new OperationError("Patch failed", {
|
|
1339
|
+
docId: id,
|
|
1340
|
+
operation: "patch"
|
|
1341
|
+
});
|
|
1342
|
+
if (doc._rev !== properties._rev) throw new ConflictError(id, { operation: "patch" });
|
|
988
1343
|
const updatedDoc = {
|
|
989
1344
|
...doc,
|
|
990
1345
|
...properties
|
|
@@ -1005,7 +1360,9 @@ const patch = async (configInput, id, _properties) => {
|
|
|
1005
1360
|
* @param properties - Properties to merge into the document
|
|
1006
1361
|
* @returns The result of the put operation or an error if max retries are exceeded
|
|
1007
1362
|
*
|
|
1008
|
-
* @throws
|
|
1363
|
+
* @throws {NotFoundError} When the document does not exist.
|
|
1364
|
+
* @throws {RetryableError} When a retryable transport or HTTP failure occurs before retries are exhausted.
|
|
1365
|
+
* @throws {OperationError} When retries are exhausted or a non-retryable operational failure occurs.
|
|
1009
1366
|
*/
|
|
1010
1367
|
const patchDangerously = async (configInput, id, properties) => {
|
|
1011
1368
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1015,65 +1372,66 @@ const patchDangerously = async (configInput, id, properties) => {
|
|
|
1015
1372
|
let attempts = 0;
|
|
1016
1373
|
logger.info(`Starting patch operation for document ${id}`);
|
|
1017
1374
|
logger.debug("Patch properties:", properties);
|
|
1375
|
+
let lastError;
|
|
1018
1376
|
while (attempts <= maxRetries) {
|
|
1019
1377
|
logger.debug(`Attempt ${attempts + 1} of ${maxRetries + 1}`);
|
|
1020
1378
|
try {
|
|
1021
|
-
const doc = await get(
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
}
|
|
1379
|
+
const doc = await get({
|
|
1380
|
+
...config,
|
|
1381
|
+
throwOnGetNotFound: true
|
|
1382
|
+
}, id);
|
|
1383
|
+
if (!doc) throw new OperationError("Patch failed", {
|
|
1384
|
+
docId: id,
|
|
1385
|
+
operation: "patchDangerously"
|
|
1386
|
+
});
|
|
1030
1387
|
const updatedDoc = {
|
|
1031
1388
|
...doc,
|
|
1032
1389
|
...properties
|
|
1033
1390
|
};
|
|
1034
1391
|
logger.debug("Merged document:", updatedDoc);
|
|
1035
1392
|
const result = await put(config, updatedDoc);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
return result;
|
|
1039
|
-
}
|
|
1040
|
-
attempts++;
|
|
1041
|
-
if (attempts > maxRetries) {
|
|
1042
|
-
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`);
|
|
1043
|
-
throw new Error(`Failed to patch after ${maxRetries} attempts`);
|
|
1044
|
-
}
|
|
1045
|
-
logger.warn(`Conflict detected for ${id}, retrying (attempt ${attempts})`);
|
|
1046
|
-
await setTimeout$1(delay);
|
|
1047
|
-
delay *= config.backoffFactor || 2;
|
|
1048
|
-
logger.debug(`Next retry delay: ${delay}ms`);
|
|
1393
|
+
logger.info(`Successfully patched document ${id}, rev: ${result.rev}`);
|
|
1394
|
+
return result;
|
|
1049
1395
|
} catch (err) {
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
ok: false,
|
|
1054
|
-
statusCode: 404,
|
|
1055
|
-
error: "not_found"
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1396
|
+
if (!(err instanceof Error)) throw err;
|
|
1397
|
+
if (!(err instanceof ConflictError) && !(err instanceof RetryableError)) throw err;
|
|
1398
|
+
lastError = err;
|
|
1058
1399
|
attempts++;
|
|
1059
1400
|
if (attempts > maxRetries) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1401
|
+
logger.error(`Failed to patch ${id} after ${maxRetries} attempts`, err);
|
|
1402
|
+
throw new OperationError("Patch failed", {
|
|
1403
|
+
cause: err,
|
|
1404
|
+
couchError: err instanceof HideABedError ? err.couchError : void 0,
|
|
1405
|
+
docId: id,
|
|
1406
|
+
operation: "patchDangerously",
|
|
1407
|
+
statusCode: err instanceof HideABedError ? err.statusCode : void 0
|
|
1408
|
+
});
|
|
1067
1409
|
}
|
|
1068
1410
|
logger.warn(`Error during patch attempt ${attempts}: ${err}`);
|
|
1069
1411
|
await setTimeout$1(delay);
|
|
1412
|
+
delay *= config.backoffFactor || 2;
|
|
1070
1413
|
logger.debug(`Retrying after ${delay}ms`);
|
|
1071
1414
|
}
|
|
1072
1415
|
}
|
|
1416
|
+
throw new OperationError("Patch failed", {
|
|
1417
|
+
cause: lastError,
|
|
1418
|
+
docId: id,
|
|
1419
|
+
operation: "patchDangerously"
|
|
1420
|
+
});
|
|
1073
1421
|
};
|
|
1074
1422
|
|
|
1075
1423
|
//#endregion
|
|
1076
1424
|
//#region impl/query.mts
|
|
1425
|
+
const ValidSchema = z$1.custom((value) => {
|
|
1426
|
+
return value !== null && typeof value === "object" && "~standard" in value;
|
|
1427
|
+
}, { message: "schema must be a valid StandardSchemaV1 schema" });
|
|
1428
|
+
const QueryValidationSchema = z$1.object({
|
|
1429
|
+
docSchema: ValidSchema.optional(),
|
|
1430
|
+
keySchema: ValidSchema.optional(),
|
|
1431
|
+
onInvalidDoc: z$1.enum(["skip", "throw"]).optional(),
|
|
1432
|
+
valueSchema: ValidSchema.optional()
|
|
1433
|
+
}).optional();
|
|
1434
|
+
const QueryOptionsSchema = ViewOptions.extend({ validate: QueryValidationSchema }).strict();
|
|
1077
1435
|
/**
|
|
1078
1436
|
* Executes a CouchDB view query with optional schema validation and automatic handling
|
|
1079
1437
|
* of HTTP method selection, query string construction, and retryable errors.
|
|
@@ -1093,69 +1451,85 @@ const patchDangerously = async (configInput, id, properties) => {
|
|
|
1093
1451
|
* @returns The parsed view response with rows validated against the supplied schemas.
|
|
1094
1452
|
*
|
|
1095
1453
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
1096
|
-
* @throws {
|
|
1097
|
-
* @throws {
|
|
1454
|
+
* @throws {ValidationError} When row, key, value, or included document validation fails.
|
|
1455
|
+
* @throws {OperationError} When CouchDB returns a non-retryable response or malformed row payload.
|
|
1098
1456
|
*/
|
|
1099
1457
|
async function query(_config, view, options = {}) {
|
|
1100
1458
|
const configParseResult = CouchConfig.safeParse(_config);
|
|
1459
|
+
const parsedOptions = QueryOptionsSchema.parse(options || {});
|
|
1101
1460
|
const logger = createLogger(_config);
|
|
1102
1461
|
logger.info(`Starting view query: ${view}`);
|
|
1103
|
-
logger.debug("Query options:",
|
|
1462
|
+
logger.debug("Query options:", parsedOptions);
|
|
1104
1463
|
if (!configParseResult.success) {
|
|
1105
1464
|
logger.error(`Invalid configuration provided: ${z$1.prettifyError(configParseResult.error)}`);
|
|
1106
1465
|
throw configParseResult.error;
|
|
1107
1466
|
}
|
|
1108
1467
|
const config = configParseResult.data;
|
|
1109
|
-
let qs = queryString(
|
|
1110
|
-
let method = "
|
|
1468
|
+
let qs = queryString(parsedOptions);
|
|
1469
|
+
let method = "GET";
|
|
1111
1470
|
let payload = null;
|
|
1112
|
-
|
|
1113
|
-
json: true,
|
|
1114
|
-
headers: { "Content-Type": "application/json" }
|
|
1115
|
-
});
|
|
1116
|
-
if (typeof options.keys !== "undefined") {
|
|
1471
|
+
if (typeof parsedOptions.keys !== "undefined") {
|
|
1117
1472
|
const MAX_URL_LENGTH = 2e3;
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const keysAsString = `keys=${JSON.stringify(options.keys)}`;
|
|
1473
|
+
const { keys, validate, ...queryableOptions } = parsedOptions;
|
|
1474
|
+
qs = queryString(queryableOptions);
|
|
1475
|
+
const keysAsString = `keys=${JSON.stringify(keys)}`;
|
|
1122
1476
|
if (keysAsString.length + qs.length + 1 <= MAX_URL_LENGTH) {
|
|
1123
|
-
method = "
|
|
1477
|
+
method = "GET";
|
|
1124
1478
|
if (qs.length > 0) qs += "&";
|
|
1125
1479
|
else qs = "";
|
|
1126
1480
|
qs += keysAsString;
|
|
1127
1481
|
} else {
|
|
1128
|
-
method = "
|
|
1129
|
-
payload = { keys:
|
|
1482
|
+
method = "POST";
|
|
1483
|
+
payload = { keys: parsedOptions.keys };
|
|
1130
1484
|
}
|
|
1131
1485
|
}
|
|
1132
1486
|
logger.debug("Generated query string:", qs);
|
|
1133
|
-
const url =
|
|
1487
|
+
const url = createCouchPathUrl(view, config.couch);
|
|
1488
|
+
if (qs) url.search = qs;
|
|
1134
1489
|
let results;
|
|
1135
1490
|
try {
|
|
1136
1491
|
logger.debug(`Sending ${method} request to: ${url}`);
|
|
1137
|
-
results =
|
|
1492
|
+
results = await fetchCouchJson({
|
|
1493
|
+
auth: config.auth,
|
|
1494
|
+
method,
|
|
1495
|
+
operation: "query",
|
|
1496
|
+
request: config.request,
|
|
1497
|
+
url,
|
|
1498
|
+
body: method === "POST" ? payload : void 0
|
|
1499
|
+
});
|
|
1138
1500
|
} catch (err) {
|
|
1139
1501
|
logger.error("Network error during query:", err);
|
|
1140
|
-
RetryableError.handleNetworkError(err);
|
|
1502
|
+
RetryableError.handleNetworkError(err, "query");
|
|
1141
1503
|
}
|
|
1142
1504
|
if (!results) {
|
|
1143
1505
|
logger.error("No response received from query request");
|
|
1144
|
-
throw new RetryableError("
|
|
1506
|
+
throw new RetryableError("Query failed", 503, { operation: "query" });
|
|
1145
1507
|
}
|
|
1146
1508
|
const body = results.body;
|
|
1147
|
-
if (
|
|
1148
|
-
logger.
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1509
|
+
if (!isSuccessStatusCode("viewQuery", results.statusCode) || body.error) {
|
|
1510
|
+
if (body.error) logger.error(`Query error: ${JSON.stringify(body)}`);
|
|
1511
|
+
else logger.error(`Unexpected status code: ${results.statusCode}`);
|
|
1512
|
+
throw createResponseError({
|
|
1513
|
+
body,
|
|
1514
|
+
defaultMessage: "Query failed",
|
|
1515
|
+
operation: "query",
|
|
1516
|
+
statusCode: results.statusCode
|
|
1517
|
+
});
|
|
1154
1518
|
}
|
|
1155
|
-
|
|
1519
|
+
const rows = options.validate && body.rows ? await parseRows(body.rows, {
|
|
1520
|
+
...options.validate,
|
|
1521
|
+
defaultMessage: "Query failed",
|
|
1522
|
+
operation: "query"
|
|
1523
|
+
}) : body.rows ?? [];
|
|
1156
1524
|
logger.info(`Successfully executed view query: ${view}`);
|
|
1157
|
-
logger.debug("Query response:",
|
|
1158
|
-
|
|
1525
|
+
logger.debug("Query response:", {
|
|
1526
|
+
...body,
|
|
1527
|
+
rows
|
|
1528
|
+
});
|
|
1529
|
+
return {
|
|
1530
|
+
...body,
|
|
1531
|
+
rows
|
|
1532
|
+
};
|
|
1159
1533
|
}
|
|
1160
1534
|
|
|
1161
1535
|
//#endregion
|
|
@@ -1167,35 +1541,35 @@ const setupEmitter = (config) => {
|
|
|
1167
1541
|
|
|
1168
1542
|
//#endregion
|
|
1169
1543
|
//#region impl/utils/transactionErrors.mts
|
|
1170
|
-
var TransactionSetupError = class extends
|
|
1544
|
+
var TransactionSetupError = class extends OperationError {
|
|
1171
1545
|
details;
|
|
1172
1546
|
constructor(message, details = {}) {
|
|
1173
|
-
super(message);
|
|
1547
|
+
super(message, { category: "transaction" });
|
|
1174
1548
|
this.name = "TransactionSetupError";
|
|
1175
1549
|
this.details = details;
|
|
1176
1550
|
}
|
|
1177
1551
|
};
|
|
1178
|
-
var TransactionVersionConflictError = class extends
|
|
1552
|
+
var TransactionVersionConflictError = class extends OperationError {
|
|
1179
1553
|
conflictingIds;
|
|
1180
1554
|
constructor(conflictingIds) {
|
|
1181
|
-
super(`Revision mismatch for documents: ${conflictingIds.join(", ")}
|
|
1555
|
+
super(`Revision mismatch for documents: ${conflictingIds.join(", ")}`, { category: "transaction" });
|
|
1182
1556
|
this.name = "TransactionVersionConflictError";
|
|
1183
1557
|
this.conflictingIds = conflictingIds;
|
|
1184
1558
|
}
|
|
1185
1559
|
};
|
|
1186
|
-
var TransactionBulkOperationError = class extends
|
|
1560
|
+
var TransactionBulkOperationError = class extends OperationError {
|
|
1187
1561
|
failedDocs;
|
|
1188
1562
|
constructor(failedDocs) {
|
|
1189
|
-
super(`Failed to save documents: ${failedDocs.map((d) => d.id).join(", ")}
|
|
1563
|
+
super(`Failed to save documents: ${failedDocs.map((d) => d.id).join(", ")}`, { category: "transaction" });
|
|
1190
1564
|
this.name = "TransactionBulkOperationError";
|
|
1191
1565
|
this.failedDocs = failedDocs;
|
|
1192
1566
|
}
|
|
1193
1567
|
};
|
|
1194
|
-
var TransactionRollbackError = class extends
|
|
1568
|
+
var TransactionRollbackError = class extends OperationError {
|
|
1195
1569
|
originalError;
|
|
1196
1570
|
rollbackResults;
|
|
1197
1571
|
constructor(message, originalError, rollbackResults) {
|
|
1198
|
-
super(message);
|
|
1572
|
+
super(message, { category: "transaction" });
|
|
1199
1573
|
this.name = "TransactionRollbackError";
|
|
1200
1574
|
this.originalError = originalError;
|
|
1201
1575
|
this.rollbackResults = rollbackResults;
|
|
@@ -1215,39 +1589,48 @@ var TransactionRollbackError = class extends Error {
|
|
|
1215
1589
|
* @returns {Promise<BulkSaveResponse>} - The response from CouchDB after the bulk save operation.
|
|
1216
1590
|
*
|
|
1217
1591
|
* @throws {RetryableError} When a retryable HTTP status code is encountered or no response is received.
|
|
1218
|
-
* @throws {
|
|
1592
|
+
* @throws {OperationError} When bulk save input is invalid or CouchDB returns a non-retryable request-level failure.
|
|
1219
1593
|
*/
|
|
1220
1594
|
const bulkSave = async (config, docs) => {
|
|
1221
|
-
const
|
|
1595
|
+
const parsedConfig = CouchConfig.parse(config);
|
|
1596
|
+
const logger = createLogger(parsedConfig);
|
|
1222
1597
|
if (docs == null || !docs.length) {
|
|
1223
1598
|
logger.error("bulkSave called with no docs");
|
|
1224
|
-
throw new
|
|
1599
|
+
throw new OperationError("Bulk save requires at least one document", { operation: "request" });
|
|
1225
1600
|
}
|
|
1226
1601
|
logger.info(`Starting bulk save of ${docs.length} documents`);
|
|
1227
|
-
const url =
|
|
1602
|
+
const url = createCouchPathUrl("_bulk_docs", parsedConfig.couch);
|
|
1228
1603
|
const body = { docs };
|
|
1229
|
-
const mergedOpts = mergeNeedleOpts(config, {
|
|
1230
|
-
json: true,
|
|
1231
|
-
headers: { "Content-Type": "application/json" }
|
|
1232
|
-
});
|
|
1233
1604
|
let resp;
|
|
1234
1605
|
try {
|
|
1235
|
-
resp = await
|
|
1606
|
+
resp = await fetchCouchJson({
|
|
1607
|
+
auth: parsedConfig.auth,
|
|
1608
|
+
method: "POST",
|
|
1609
|
+
operation: "request",
|
|
1610
|
+
request: parsedConfig.request,
|
|
1611
|
+
url,
|
|
1612
|
+
body
|
|
1613
|
+
});
|
|
1236
1614
|
} catch (err) {
|
|
1237
1615
|
logger.error("Network error during bulk save:", err);
|
|
1238
|
-
RetryableError.handleNetworkError(err);
|
|
1616
|
+
RetryableError.handleNetworkError(err, "request");
|
|
1239
1617
|
}
|
|
1240
1618
|
if (!resp) {
|
|
1241
1619
|
logger.error("No response received from bulk save request");
|
|
1242
|
-
throw new RetryableError("
|
|
1620
|
+
throw new RetryableError("Bulk save failed", 503, { operation: "request" });
|
|
1243
1621
|
}
|
|
1244
1622
|
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1245
1623
|
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1246
|
-
throw new RetryableError("
|
|
1624
|
+
throw new RetryableError("Bulk save failed", resp.statusCode, { operation: "request" });
|
|
1247
1625
|
}
|
|
1248
|
-
if (resp.statusCode
|
|
1626
|
+
if (!isSuccessStatusCode("bulkSave", resp.statusCode)) {
|
|
1249
1627
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1250
|
-
throw
|
|
1628
|
+
throw createResponseError({
|
|
1629
|
+
body: resp.body,
|
|
1630
|
+
defaultMessage: "Bulk save failed",
|
|
1631
|
+
operation: "request",
|
|
1632
|
+
statusCode: resp.statusCode
|
|
1633
|
+
});
|
|
1251
1634
|
}
|
|
1252
1635
|
const results = resp?.body || [];
|
|
1253
1636
|
return BulkSaveResponse.parse(results);
|
|
@@ -1310,16 +1693,21 @@ const bulkSaveTransaction = async (config, transactionId, docs) => {
|
|
|
1310
1693
|
changes: docs,
|
|
1311
1694
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1312
1695
|
};
|
|
1313
|
-
let transactionResponse
|
|
1696
|
+
let transactionResponse;
|
|
1697
|
+
try {
|
|
1698
|
+
transactionResponse = await _put(transactionDoc);
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
if (error instanceof ConflictError) throw new TransactionSetupError("Failed to create transaction document", {
|
|
1701
|
+
error: error.couchError,
|
|
1702
|
+
response: error
|
|
1703
|
+
});
|
|
1704
|
+
throw error;
|
|
1705
|
+
}
|
|
1314
1706
|
logger.debug("Transaction document created:", transactionDoc, transactionResponse);
|
|
1315
1707
|
await emitter.emit("transaction-created", {
|
|
1316
1708
|
transactionResponse,
|
|
1317
1709
|
txnDoc: transactionDoc
|
|
1318
1710
|
});
|
|
1319
|
-
if (transactionResponse.error) throw new TransactionSetupError("Failed to create transaction document", {
|
|
1320
|
-
error: transactionResponse.error,
|
|
1321
|
-
response: transactionResponse
|
|
1322
|
-
});
|
|
1323
1711
|
const existingDocs = await bulkGetDictionary(config, docs.map((d) => d._id));
|
|
1324
1712
|
logger.debug("Fetched current revisions of documents:", existingDocs);
|
|
1325
1713
|
await emitter.emit("transaction-revs-fetched", existingDocs);
|
|
@@ -1412,44 +1800,44 @@ const bulkSaveTransaction = async (config, transactionId, docs) => {
|
|
|
1412
1800
|
const remove = async (configInput, id, rev) => {
|
|
1413
1801
|
const config = CouchConfig.parse(configInput);
|
|
1414
1802
|
const logger = createLogger(config);
|
|
1415
|
-
const url =
|
|
1416
|
-
|
|
1417
|
-
json: true,
|
|
1418
|
-
headers: { "Content-Type": "application/json" }
|
|
1419
|
-
});
|
|
1803
|
+
const url = createCouchDocUrl(id, config.couch);
|
|
1804
|
+
url.searchParams.set("rev", rev);
|
|
1420
1805
|
logger.info(`Deleting document with id: ${id}`);
|
|
1421
1806
|
let resp;
|
|
1422
1807
|
try {
|
|
1423
|
-
resp = await
|
|
1808
|
+
resp = await fetchCouchJson({
|
|
1809
|
+
auth: config.auth,
|
|
1810
|
+
method: "DELETE",
|
|
1811
|
+
operation: "remove",
|
|
1812
|
+
request: config.request,
|
|
1813
|
+
url
|
|
1814
|
+
});
|
|
1424
1815
|
} catch (err) {
|
|
1425
1816
|
logger.error("Error during delete operation:", err);
|
|
1426
|
-
RetryableError.handleNetworkError(err);
|
|
1817
|
+
RetryableError.handleNetworkError(err, "remove");
|
|
1427
1818
|
}
|
|
1428
1819
|
if (!resp) {
|
|
1429
1820
|
logger.error("No response received from delete request");
|
|
1430
|
-
throw new RetryableError("
|
|
1431
|
-
}
|
|
1432
|
-
let result;
|
|
1433
|
-
if (typeof resp.body === "string") try {
|
|
1434
|
-
result = JSON.parse(resp.body);
|
|
1435
|
-
} catch {
|
|
1436
|
-
result = {};
|
|
1821
|
+
throw new RetryableError("Remove failed", 503, { operation: "remove" });
|
|
1437
1822
|
}
|
|
1438
|
-
|
|
1823
|
+
const result = { ...isRecord(resp.body) ? resp.body : {} };
|
|
1439
1824
|
result.statusCode = resp.statusCode;
|
|
1440
1825
|
if (resp.statusCode === 404) {
|
|
1441
1826
|
logger.warn(`Document not found for deletion: ${id}`);
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1447
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1448
|
-
throw new RetryableError(result.reason || "retryable error", resp.statusCode);
|
|
1827
|
+
throw new NotFoundError(id, {
|
|
1828
|
+
operation: "remove",
|
|
1829
|
+
statusCode: resp.statusCode
|
|
1830
|
+
});
|
|
1449
1831
|
}
|
|
1450
|
-
if (resp.statusCode
|
|
1832
|
+
if (!isSuccessStatusCode("documentDelete", resp.statusCode) || !result.ok) {
|
|
1451
1833
|
logger.error(`Unexpected status code: ${resp.statusCode}`);
|
|
1452
|
-
throw
|
|
1834
|
+
throw createResponseError({
|
|
1835
|
+
body: resp.body,
|
|
1836
|
+
defaultMessage: "Remove failed",
|
|
1837
|
+
docId: id,
|
|
1838
|
+
operation: "remove",
|
|
1839
|
+
statusCode: resp.statusCode
|
|
1840
|
+
});
|
|
1453
1841
|
}
|
|
1454
1842
|
logger.info(`Successfully deleted document: ${id}`);
|
|
1455
1843
|
return CouchPutResponse.parse(result);
|
|
@@ -1479,7 +1867,8 @@ const remove = async (configInput, id, rev) => {
|
|
|
1479
1867
|
* console.log(results);
|
|
1480
1868
|
* ```
|
|
1481
1869
|
*
|
|
1482
|
-
* @throws
|
|
1870
|
+
* @throws {RetryableError} When the bulk request fails with a retryable transport or HTTP error.
|
|
1871
|
+
* @throws {OperationError} When CouchDB returns a non-retryable request-level failure.
|
|
1483
1872
|
*/
|
|
1484
1873
|
const bulkRemove = async (configInput, ids) => {
|
|
1485
1874
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1521,7 +1910,8 @@ const bulkRemove = async (configInput, ids) => {
|
|
|
1521
1910
|
* console.log(results);
|
|
1522
1911
|
* ```
|
|
1523
1912
|
*
|
|
1524
|
-
* @throws
|
|
1913
|
+
* @throws {RetryableError} When a request-level transport or retryable HTTP failure occurs.
|
|
1914
|
+
* @throws {OperationError} When a request fails in a non-retryable way.
|
|
1525
1915
|
*/
|
|
1526
1916
|
const bulkRemoveMap = async (configInput, ids) => {
|
|
1527
1917
|
const config = CouchConfig.parse(configInput);
|
|
@@ -1550,7 +1940,7 @@ const bulkRemoveMap = async (configInput, ids) => {
|
|
|
1550
1940
|
* @param configInput - The CouchDB configuration input.
|
|
1551
1941
|
* @returns A promise that resolves to the CouchDB database information.
|
|
1552
1942
|
* @throws {RetryableError} `RetryableError` If a retryable error occurs during the request.
|
|
1553
|
-
* @throws {
|
|
1943
|
+
* @throws {OperationError} `OperationError` For other non-retryable response failures.
|
|
1554
1944
|
*
|
|
1555
1945
|
* @example
|
|
1556
1946
|
* ```ts
|
|
@@ -1570,27 +1960,34 @@ const bulkRemoveMap = async (configInput, ids) => {
|
|
|
1570
1960
|
const getDBInfo = async (configInput) => {
|
|
1571
1961
|
const config = CouchConfig.parse(configInput);
|
|
1572
1962
|
const logger = createLogger(config);
|
|
1573
|
-
const url =
|
|
1963
|
+
const url = createCouchDbUrl(config.couch);
|
|
1574
1964
|
let resp;
|
|
1575
1965
|
try {
|
|
1576
|
-
resp = await
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1966
|
+
resp = await fetchCouchJson({
|
|
1967
|
+
auth: config.auth,
|
|
1968
|
+
method: "GET",
|
|
1969
|
+
operation: "getDBInfo",
|
|
1970
|
+
request: config.request,
|
|
1971
|
+
url
|
|
1972
|
+
});
|
|
1973
|
+
if (!isSuccessStatusCode("database", resp.statusCode)) {
|
|
1974
|
+
logger.error(`Non-success status code received: ${resp.statusCode}`);
|
|
1975
|
+
throw createResponseError({
|
|
1976
|
+
body: resp.body,
|
|
1977
|
+
defaultMessage: "Failed to fetch database info",
|
|
1978
|
+
operation: "getDBInfo",
|
|
1979
|
+
statusCode: resp.statusCode
|
|
1980
|
+
});
|
|
1981
|
+
}
|
|
1580
1982
|
} catch (err) {
|
|
1581
1983
|
logger.error("Error during get operation:", err);
|
|
1582
|
-
RetryableError.handleNetworkError(err);
|
|
1984
|
+
RetryableError.handleNetworkError(err, "getDBInfo");
|
|
1583
1985
|
}
|
|
1584
1986
|
if (!resp) {
|
|
1585
1987
|
logger.error("No response received from get request");
|
|
1586
|
-
throw new RetryableError("
|
|
1988
|
+
throw new RetryableError("Failed to fetch database info", 503, { operation: "getDBInfo" });
|
|
1587
1989
|
}
|
|
1588
|
-
|
|
1589
|
-
if (RetryableError.isRetryableStatusCode(resp.statusCode)) {
|
|
1590
|
-
logger.warn(`Retryable status code received: ${resp.statusCode}`);
|
|
1591
|
-
throw new RetryableError(result.reason ?? "retryable error", resp.statusCode);
|
|
1592
|
-
}
|
|
1593
|
-
return CouchDBInfo.parse(result);
|
|
1990
|
+
return CouchDBInfo.parse(resp.body);
|
|
1594
1991
|
};
|
|
1595
1992
|
|
|
1596
1993
|
//#endregion
|
|
@@ -1686,7 +2083,7 @@ async function removeLock(configInput, docId, lockOptions) {
|
|
|
1686
2083
|
|
|
1687
2084
|
//#endregion
|
|
1688
2085
|
//#region schema/sugar/watch.mts
|
|
1689
|
-
const WatchOptions = z$1.
|
|
2086
|
+
const WatchOptions = z$1.strictObject({
|
|
1690
2087
|
include_docs: z$1.boolean().default(false),
|
|
1691
2088
|
maxRetries: z$1.number().describe("maximum number of retries before giving up"),
|
|
1692
2089
|
initialDelay: z$1.number().describe("initial delay between retries in milliseconds"),
|
|
@@ -1712,112 +2109,155 @@ function watchDocs(configInput, docIds, onChange, optionsInput = {}) {
|
|
|
1712
2109
|
const options = WatchOptions.parse(optionsInput);
|
|
1713
2110
|
const logger = createLogger(config);
|
|
1714
2111
|
const emitter = new EventEmitter();
|
|
2112
|
+
const request = config.request;
|
|
1715
2113
|
let lastSeq = null;
|
|
1716
2114
|
let stopping = false;
|
|
2115
|
+
let stopEndEmitted = false;
|
|
1717
2116
|
let retryCount = 0;
|
|
1718
|
-
|
|
2117
|
+
const lifecycleAbortController = new AbortController();
|
|
2118
|
+
let currentAbortController = null;
|
|
1719
2119
|
const maxRetries = options.maxRetries || 10;
|
|
1720
2120
|
const initialDelay = options.initialDelay || 1e3;
|
|
1721
2121
|
const maxDelay = options.maxDelay || 3e4;
|
|
1722
2122
|
const _docIds = Array.isArray(docIds) ? docIds : [docIds];
|
|
1723
|
-
if (_docIds.length === 0) throw new
|
|
1724
|
-
if (_docIds.length > 100) throw new
|
|
2123
|
+
if (_docIds.length === 0) throw new OperationError("docIds must be a non-empty array", { operation: "watchDocs" });
|
|
2124
|
+
if (_docIds.length > 100) throw new OperationError("docIds must be an array of 100 or fewer elements", { operation: "watchDocs" });
|
|
2125
|
+
const emitStopEnd = () => {
|
|
2126
|
+
if (stopEndEmitted) return;
|
|
2127
|
+
stopEndEmitted = true;
|
|
2128
|
+
emitter.emit("end", { lastSeq });
|
|
2129
|
+
};
|
|
2130
|
+
const stopWatching = () => {
|
|
2131
|
+
if (stopping) return;
|
|
2132
|
+
stopping = true;
|
|
2133
|
+
lifecycleAbortController.abort();
|
|
2134
|
+
currentAbortController?.abort();
|
|
2135
|
+
request?.signal?.removeEventListener("abort", handleExternalAbort);
|
|
2136
|
+
emitStopEnd();
|
|
2137
|
+
emitter.removeAllListeners();
|
|
2138
|
+
};
|
|
2139
|
+
const handleExternalAbort = () => {
|
|
2140
|
+
logger.info(`Request signal aborted, stopping watcher for [${_docIds}]`);
|
|
2141
|
+
stopWatching();
|
|
2142
|
+
};
|
|
2143
|
+
request?.signal?.addEventListener("abort", handleExternalAbort, { once: true });
|
|
1725
2144
|
const connect = async () => {
|
|
1726
2145
|
if (stopping) return;
|
|
1727
2146
|
const feed = "continuous";
|
|
1728
2147
|
const includeDocs = options.include_docs ?? false;
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2148
|
+
const url = createCouchPathUrl("_changes", config.couch);
|
|
2149
|
+
url.searchParams.set("feed", feed);
|
|
2150
|
+
url.searchParams.set("since", String(lastSeq));
|
|
2151
|
+
url.searchParams.set("include_docs", String(includeDocs));
|
|
2152
|
+
url.searchParams.set("filter", "_doc_ids");
|
|
2153
|
+
url.searchParams.set("doc_ids", JSON.stringify(_docIds));
|
|
2154
|
+
const abortController = new AbortController();
|
|
2155
|
+
currentAbortController = abortController;
|
|
1736
2156
|
let buffer = "";
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
const lines = buffer.split("\n");
|
|
1741
|
-
buffer = lines.pop() || "";
|
|
1742
|
-
for (const line of lines) if (line.trim()) try {
|
|
2157
|
+
const processLine = (line) => {
|
|
2158
|
+
if (!line.trim()) return;
|
|
2159
|
+
try {
|
|
1743
2160
|
const change = JSON.parse(line);
|
|
1744
|
-
if (!change.id) return
|
|
2161
|
+
if (!change.id) return;
|
|
1745
2162
|
logger.debug(`Change detected, watching [${_docIds}]`, change);
|
|
1746
2163
|
lastSeq = change.seq || change.last_seq;
|
|
1747
2164
|
emitter.emit("change", change);
|
|
1748
2165
|
} catch (err) {
|
|
1749
2166
|
logger.error("Error parsing change:", err, "Line:", line);
|
|
1750
2167
|
}
|
|
1751
|
-
}
|
|
1752
|
-
|
|
2168
|
+
};
|
|
2169
|
+
try {
|
|
2170
|
+
const response = await fetchCouchStream({
|
|
2171
|
+
auth: config.auth,
|
|
2172
|
+
method: "GET",
|
|
2173
|
+
operation: "watchDocs",
|
|
2174
|
+
url,
|
|
2175
|
+
headers: { "Content-Type": "application/json" },
|
|
2176
|
+
request,
|
|
2177
|
+
signal: AbortSignal.any([abortController.signal, lifecycleAbortController.signal])
|
|
2178
|
+
});
|
|
1753
2179
|
logger.debug(`Received response with status code, watching [${_docIds}]: ${response.statusCode}`);
|
|
1754
2180
|
if (RetryableError.isRetryableStatusCode(response.statusCode)) {
|
|
1755
2181
|
logger.warn(`Retryable status code received: ${response.statusCode}`);
|
|
1756
|
-
|
|
1757
|
-
handleReconnect();
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2182
|
+
abortController.abort();
|
|
2183
|
+
await handleReconnect();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (!isSuccessStatusCode("changesFeed", response.statusCode)) {
|
|
2187
|
+
emitter.emit("error", createResponseError({
|
|
2188
|
+
defaultMessage: "Watch request failed",
|
|
2189
|
+
operation: "watchDocs",
|
|
2190
|
+
statusCode: response.statusCode
|
|
2191
|
+
}));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
retryCount = 0;
|
|
2195
|
+
if (!response.body) throw new RetryableError("Watch request failed", 503, { operation: "watchDocs" });
|
|
2196
|
+
const reader = response.body.getReader();
|
|
2197
|
+
const decoder = new TextDecoder();
|
|
2198
|
+
while (!stopping) {
|
|
2199
|
+
const { done, value } = await reader.read();
|
|
2200
|
+
if (done) break;
|
|
2201
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2202
|
+
const lines = buffer.split("\n");
|
|
2203
|
+
buffer = lines.pop() || "";
|
|
2204
|
+
lines.forEach(processLine);
|
|
2205
|
+
}
|
|
2206
|
+
if (stopping || abortController.signal.aborted) return;
|
|
2207
|
+
buffer += decoder.decode();
|
|
2208
|
+
if (buffer.trim()) processLine(buffer);
|
|
2209
|
+
logger.info("Stream completed. Last seen seq: ", lastSeq);
|
|
2210
|
+
emitter.emit("end", { lastSeq });
|
|
2211
|
+
if (!stopping) await handleReconnect();
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
if (stopping || abortController.signal.aborted) {
|
|
1762
2214
|
logger.info("stopping in progress, ignore stream error");
|
|
1763
2215
|
return;
|
|
1764
2216
|
}
|
|
1765
|
-
logger.error(`Network error during stream, watching [${_docIds}]:`, err
|
|
2217
|
+
logger.error(`Network error during stream, watching [${_docIds}]:`, String(err));
|
|
1766
2218
|
try {
|
|
1767
|
-
RetryableError.handleNetworkError(err);
|
|
2219
|
+
RetryableError.handleNetworkError(err, "watchDocs");
|
|
1768
2220
|
} catch (filteredError) {
|
|
1769
2221
|
if (filteredError instanceof RetryableError) {
|
|
1770
2222
|
logger.info(`Retryable error, watching [${_docIds}]:`, filteredError.toString());
|
|
1771
|
-
handleReconnect();
|
|
2223
|
+
await handleReconnect();
|
|
1772
2224
|
} else {
|
|
1773
|
-
logger.error(`Non-retryable error, watching [${_docIds}]`, filteredError
|
|
2225
|
+
logger.error(`Non-retryable error, watching [${_docIds}]`, String(filteredError));
|
|
1774
2226
|
emitter.emit("error", filteredError);
|
|
1775
2227
|
}
|
|
1776
2228
|
}
|
|
1777
|
-
}
|
|
1778
|
-
currentRequest.on("end", () => {
|
|
1779
|
-
if (buffer.trim()) try {
|
|
1780
|
-
const change = JSON.parse(buffer);
|
|
1781
|
-
logger.debug("Final change detected:", change);
|
|
1782
|
-
emitter.emit("change", change);
|
|
1783
|
-
} catch (err) {
|
|
1784
|
-
logger.error("Error parsing final change:", err);
|
|
1785
|
-
}
|
|
1786
|
-
logger.info("Stream completed. Last seen seq: ", lastSeq);
|
|
1787
|
-
emitter.emit("end", { lastSeq });
|
|
1788
|
-
if (!stopping) handleReconnect();
|
|
1789
|
-
});
|
|
2229
|
+
}
|
|
1790
2230
|
};
|
|
1791
2231
|
const handleReconnect = async () => {
|
|
1792
2232
|
if (stopping || retryCount >= maxRetries) {
|
|
1793
2233
|
if (retryCount >= maxRetries) {
|
|
1794
2234
|
logger.error(`Max retries (${maxRetries}) reached, giving up`);
|
|
1795
|
-
emitter.emit("error",
|
|
2235
|
+
emitter.emit("error", new OperationError("Watch retries exhausted", { operation: "watchDocs" }));
|
|
1796
2236
|
}
|
|
1797
2237
|
return;
|
|
1798
2238
|
}
|
|
1799
2239
|
const delay = Math.min(initialDelay * Math.pow(2, retryCount), maxDelay);
|
|
1800
2240
|
retryCount++;
|
|
1801
2241
|
logger.info(`Attempting to reconnect in ${delay}ms (attempt ${retryCount} of ${maxRetries})`);
|
|
1802
|
-
await setTimeout$1(delay);
|
|
1803
2242
|
try {
|
|
1804
|
-
|
|
2243
|
+
await setTimeout$1(delay, void 0, { signal: lifecycleAbortController.signal });
|
|
2244
|
+
} catch {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
await connect();
|
|
1805
2249
|
} catch (err) {
|
|
1806
2250
|
logger.error("Error during reconnection:", err);
|
|
1807
|
-
handleReconnect();
|
|
2251
|
+
await handleReconnect();
|
|
1808
2252
|
}
|
|
1809
2253
|
};
|
|
1810
|
-
connect();
|
|
1811
2254
|
emitter.on("change", onChange);
|
|
2255
|
+
if (request?.signal?.aborted) stopWatching();
|
|
2256
|
+
else connect();
|
|
1812
2257
|
return {
|
|
1813
2258
|
on: (event, listener) => emitter.on(event, listener),
|
|
1814
2259
|
removeListener: (event, listener) => emitter.removeListener(event, listener),
|
|
1815
|
-
stop:
|
|
1816
|
-
stopping = true;
|
|
1817
|
-
if (currentRequest) currentRequest.destroy();
|
|
1818
|
-
emitter.emit("end", { lastSeq });
|
|
1819
|
-
emitter.removeAllListeners();
|
|
1820
|
-
}
|
|
2260
|
+
stop: stopWatching
|
|
1821
2261
|
};
|
|
1822
2262
|
}
|
|
1823
2263
|
|
|
@@ -1895,4 +2335,4 @@ function doBind(config) {
|
|
|
1895
2335
|
}
|
|
1896
2336
|
|
|
1897
2337
|
//#endregion
|
|
1898
|
-
export { QueryBuilder, bindConfig, bulkGet, bulkGetDictionary, bulkRemove, bulkRemoveMap, bulkSave, bulkSaveTransaction, createLock, createQuery, get, getAtRev, getDBInfo, patch, patchDangerously, put, query, queryStream, remove, removeLock, watchDocs, withRetry };
|
|
2338
|
+
export { ConflictError, HideABedError, NotFoundError, OperationError, QueryBuilder, RetryableError, ValidationError, bindConfig, bulkGet, bulkGetDictionary, bulkRemove, bulkRemoveMap, bulkSave, bulkSaveTransaction, createLock, createQuery, get, getAtRev, getDBInfo, patch, patchDangerously, put, query, queryStream, remove, removeLock, watchDocs, withRetry };
|