@utaba/deep-memory-storage-cosmosdb 0.10.0 → 0.12.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/dist/index.cjs +240 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -8
- package/dist/index.d.ts +0 -8
- package/dist/index.js +229 -49
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -22,14 +22,6 @@ interface CosmosDbProviderConfig {
|
|
|
22
22
|
defaultTimeoutMs?: number;
|
|
23
23
|
/** Whether to reject unauthorized TLS certs — set false for emulator (default: true) */
|
|
24
24
|
rejectUnauthorized?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Milliseconds to pause between batches in deleteAllContents/deleteRepository
|
|
27
|
-
* to pace RU consumption. Required on low-RU tiers (e.g. CosmosDB free tier at
|
|
28
|
-
* 1000 RU/s), where a single batch drop query can consume the entire per-second
|
|
29
|
-
* budget and throttle the next batch. Set to 1000+ on constrained tiers.
|
|
30
|
-
* Default: 0 (no pacing — suitable for autoscale and provisioned throughput).
|
|
31
|
-
*/
|
|
32
|
-
deleteBatchDelayMs?: number;
|
|
33
25
|
/**
|
|
34
26
|
* Optional usage sink. When provided, the provider emits one
|
|
35
27
|
* {@link OperationUsage} record per public method call with the aggregated
|
package/dist/index.d.ts
CHANGED
|
@@ -22,14 +22,6 @@ interface CosmosDbProviderConfig {
|
|
|
22
22
|
defaultTimeoutMs?: number;
|
|
23
23
|
/** Whether to reject unauthorized TLS certs — set false for emulator (default: true) */
|
|
24
24
|
rejectUnauthorized?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Milliseconds to pause between batches in deleteAllContents/deleteRepository
|
|
27
|
-
* to pace RU consumption. Required on low-RU tiers (e.g. CosmosDB free tier at
|
|
28
|
-
* 1000 RU/s), where a single batch drop query can consume the entire per-second
|
|
29
|
-
* budget and throttle the next batch. Set to 1000+ on constrained tiers.
|
|
30
|
-
* Default: 0 (no pacing — suitable for autoscale and provisioned throughput).
|
|
31
|
-
*/
|
|
32
|
-
deleteBatchDelayMs?: number;
|
|
33
25
|
/**
|
|
34
26
|
* Optional usage sink. When provided, the provider emits one
|
|
35
27
|
* {@link OperationUsage} record per public method call with the aggregated
|
package/dist/index.js
CHANGED
|
@@ -92,24 +92,11 @@ function isTransientError(err) {
|
|
|
92
92
|
return false;
|
|
93
93
|
}
|
|
94
94
|
function getRetryAfterMs(err, attempt) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return Math.max(topLevel, exponentialMs);
|
|
95
|
+
const retryAfter = err?.["retryAfterMs"];
|
|
96
|
+
if (typeof retryAfter === "number" && retryAfter > 0) {
|
|
97
|
+
return retryAfter;
|
|
99
98
|
}
|
|
100
|
-
|
|
101
|
-
let hintVal;
|
|
102
|
-
if (attrs instanceof Map) {
|
|
103
|
-
hintVal = attrs.get("x-ms-retry-after-ms") ?? attrs.get("x-ms-retry-after");
|
|
104
|
-
} else if (typeof attrs === "object" && attrs !== null) {
|
|
105
|
-
const record = attrs;
|
|
106
|
-
hintVal = record["x-ms-retry-after-ms"] ?? record["x-ms-retry-after"];
|
|
107
|
-
}
|
|
108
|
-
const hintMs = Number(hintVal);
|
|
109
|
-
if (Number.isFinite(hintMs) && hintMs > 0) {
|
|
110
|
-
return Math.max(hintMs, exponentialMs);
|
|
111
|
-
}
|
|
112
|
-
return exponentialMs;
|
|
99
|
+
return Math.min(500 * Math.pow(2, attempt), 1e4);
|
|
113
100
|
}
|
|
114
101
|
function extractRequestCharge(resultSet) {
|
|
115
102
|
const attrs = resultSet.attributes;
|
|
@@ -393,11 +380,7 @@ async function updateRepository(conn, repositoryId, updates) {
|
|
|
393
380
|
return await getRepository(conn, repositoryId);
|
|
394
381
|
}
|
|
395
382
|
var DELETE_BATCH_SIZE = 500;
|
|
396
|
-
function
|
|
397
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
398
|
-
}
|
|
399
|
-
async function deleteRepository(conn, repositoryId, onProgress, options) {
|
|
400
|
-
const batchDelayMs = options?.batchDelayMs ?? 0;
|
|
383
|
+
async function deleteRepository(conn, repositoryId, onProgress) {
|
|
401
384
|
const entityCountResult = await conn.submit(
|
|
402
385
|
"g.V().has('repositoryId', rid).has('entityType').count()",
|
|
403
386
|
{ rid: repositoryId }
|
|
@@ -423,7 +406,6 @@ async function deleteRepository(conn, repositoryId, onProgress, options) {
|
|
|
423
406
|
relationshipsDeleted = totalRelationships - remainingCount;
|
|
424
407
|
await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
|
|
425
408
|
if (remainingCount === 0) break;
|
|
426
|
-
if (batchDelayMs > 0) await sleep2(batchDelayMs);
|
|
427
409
|
}
|
|
428
410
|
while (true) {
|
|
429
411
|
await conn.submit(
|
|
@@ -438,11 +420,9 @@ async function deleteRepository(conn, repositoryId, onProgress, options) {
|
|
|
438
420
|
entitiesDeleted = totalEntities - remainingCount;
|
|
439
421
|
await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
|
|
440
422
|
if (remainingCount === 0) break;
|
|
441
|
-
if (batchDelayMs > 0) await sleep2(batchDelayMs);
|
|
442
423
|
}
|
|
443
424
|
}
|
|
444
|
-
async function deleteAllContents(conn, repositoryId, onProgress
|
|
445
|
-
const batchDelayMs = options?.batchDelayMs ?? 0;
|
|
425
|
+
async function deleteAllContents(conn, repositoryId, onProgress) {
|
|
446
426
|
const entityCountResult = await conn.submit(
|
|
447
427
|
"g.V().has('repositoryId', rid).has('entityType').count()",
|
|
448
428
|
{ rid: repositoryId }
|
|
@@ -468,7 +448,6 @@ async function deleteAllContents(conn, repositoryId, onProgress, options) {
|
|
|
468
448
|
relationshipsDeleted = totalRelationships - remainingCount;
|
|
469
449
|
await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
|
|
470
450
|
if (remainingCount === 0) break;
|
|
471
|
-
if (batchDelayMs > 0) await sleep2(batchDelayMs);
|
|
472
451
|
}
|
|
473
452
|
while (true) {
|
|
474
453
|
await conn.submit(
|
|
@@ -483,7 +462,6 @@ async function deleteAllContents(conn, repositoryId, onProgress, options) {
|
|
|
483
462
|
entitiesDeleted = totalEntities - remainingCount;
|
|
484
463
|
await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
|
|
485
464
|
if (remainingCount === 0) break;
|
|
486
|
-
if (batchDelayMs > 0) await sleep2(batchDelayMs);
|
|
487
465
|
}
|
|
488
466
|
return { deletedEntities: totalEntities, deletedRelationships: totalRelationships };
|
|
489
467
|
}
|
|
@@ -1110,26 +1088,229 @@ function isInTimeRange(timestamp, timeRange) {
|
|
|
1110
1088
|
return timestamp >= timeRange.from && timestamp <= timeRange.to;
|
|
1111
1089
|
}
|
|
1112
1090
|
|
|
1113
|
-
// src/queries/
|
|
1114
|
-
|
|
1115
|
-
var
|
|
1116
|
-
|
|
1091
|
+
// src/queries/adaptive-import.ts
|
|
1092
|
+
import { ImportThrottleAbortError } from "@utaba/deep-memory";
|
|
1093
|
+
var DEFAULT_MIN = 2;
|
|
1094
|
+
var DEFAULT_START = 5;
|
|
1095
|
+
var DEFAULT_MAX = 32;
|
|
1096
|
+
var DEFAULT_INCREASE_AFTER = 50;
|
|
1097
|
+
var DEFAULT_COOLDOWN_MS = 1e3;
|
|
1098
|
+
var DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN = 10;
|
|
1099
|
+
function resolveOptions(opts) {
|
|
1100
|
+
const min = Math.max(1, opts?.min ?? DEFAULT_MIN);
|
|
1101
|
+
const max = Math.max(min, opts?.max ?? DEFAULT_MAX);
|
|
1102
|
+
const start = Math.min(max, Math.max(min, opts?.start ?? DEFAULT_START));
|
|
1103
|
+
const increaseAfter = Math.max(1, opts?.increaseAfter ?? DEFAULT_INCREASE_AFTER);
|
|
1104
|
+
const cooldownMs = Math.max(0, opts?.cooldownMs ?? DEFAULT_COOLDOWN_MS);
|
|
1105
|
+
const maxConsecutiveThrottlesAtMin = Math.max(
|
|
1106
|
+
1,
|
|
1107
|
+
opts?.maxConsecutiveThrottlesAtMin ?? DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN
|
|
1108
|
+
);
|
|
1109
|
+
return {
|
|
1110
|
+
min,
|
|
1111
|
+
start,
|
|
1112
|
+
max,
|
|
1113
|
+
increaseAfter,
|
|
1114
|
+
cooldownMs,
|
|
1115
|
+
maxConsecutiveThrottlesAtMin,
|
|
1116
|
+
onAdjust: opts?.onAdjust
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
var AdaptiveConcurrencyController = class {
|
|
1120
|
+
opts;
|
|
1121
|
+
current;
|
|
1122
|
+
streak = 0;
|
|
1123
|
+
cooldownUntil = 0;
|
|
1124
|
+
completed = 0;
|
|
1125
|
+
throttled = 0;
|
|
1126
|
+
startEmitted = false;
|
|
1127
|
+
consecutiveThrottlesAtMin = 0;
|
|
1128
|
+
constructor(opts) {
|
|
1129
|
+
this.opts = resolveOptions(opts);
|
|
1130
|
+
this.current = this.opts.start;
|
|
1131
|
+
}
|
|
1132
|
+
/** Current target concurrency. */
|
|
1133
|
+
getConcurrency() {
|
|
1134
|
+
return this.current;
|
|
1135
|
+
}
|
|
1136
|
+
/** Configured ceiling — used by the runner to size its worker pool. */
|
|
1137
|
+
getMaxConcurrency() {
|
|
1138
|
+
return this.opts.max;
|
|
1139
|
+
}
|
|
1140
|
+
/** Total tasks that have been noted as completed (success or throttle). */
|
|
1141
|
+
getCompleted() {
|
|
1142
|
+
return this.completed;
|
|
1143
|
+
}
|
|
1144
|
+
/** Total tasks that observed at least one throttle. */
|
|
1145
|
+
getThrottledCount() {
|
|
1146
|
+
return this.throttled;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Earliest time (ms since epoch) at which a new task may be dispatched. Zero
|
|
1150
|
+
* if there is no active cooldown.
|
|
1151
|
+
*/
|
|
1152
|
+
getCooldownUntil() {
|
|
1153
|
+
return this.cooldownUntil;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Emit the initial `start` event the first time the controller is queried
|
|
1157
|
+
* for adjustment events. Kept separate so construction has no side effects.
|
|
1158
|
+
*/
|
|
1159
|
+
emitStartIfNeeded() {
|
|
1160
|
+
if (this.startEmitted) return;
|
|
1161
|
+
this.startEmitted = true;
|
|
1162
|
+
this.notify("start", this.current);
|
|
1163
|
+
}
|
|
1164
|
+
/** Record a throttle-free task completion. May trigger ramp-up. */
|
|
1165
|
+
noteSuccess() {
|
|
1166
|
+
this.completed++;
|
|
1167
|
+
this.streak++;
|
|
1168
|
+
this.consecutiveThrottlesAtMin = 0;
|
|
1169
|
+
if (this.streak >= this.opts.increaseAfter && this.current < this.opts.max) {
|
|
1170
|
+
const previous = this.current;
|
|
1171
|
+
this.current = Math.min(this.opts.max, this.current + 1);
|
|
1172
|
+
this.streak = 0;
|
|
1173
|
+
this.notify("ramp-up", previous);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/** Record a task that observed at least one throttle. Halves concurrency. */
|
|
1177
|
+
noteThrottle(now = Date.now()) {
|
|
1178
|
+
this.completed++;
|
|
1179
|
+
this.throttled++;
|
|
1180
|
+
this.streak = 0;
|
|
1181
|
+
const previous = this.current;
|
|
1182
|
+
const next = Math.max(this.opts.min, Math.floor(this.current / 2));
|
|
1183
|
+
this.cooldownUntil = now + this.opts.cooldownMs;
|
|
1184
|
+
if (next !== previous) {
|
|
1185
|
+
this.current = next;
|
|
1186
|
+
this.notify("throttle", previous);
|
|
1187
|
+
}
|
|
1188
|
+
if (this.current === this.opts.min) {
|
|
1189
|
+
this.consecutiveThrottlesAtMin++;
|
|
1190
|
+
} else {
|
|
1191
|
+
this.consecutiveThrottlesAtMin = 0;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/** Configured circuit-breaker threshold. */
|
|
1195
|
+
getMaxConsecutiveThrottlesAtMin() {
|
|
1196
|
+
return this.opts.maxConsecutiveThrottlesAtMin;
|
|
1197
|
+
}
|
|
1198
|
+
/** How many consecutive throttles have occurred at min. */
|
|
1199
|
+
getConsecutiveThrottlesAtMin() {
|
|
1200
|
+
return this.consecutiveThrottlesAtMin;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Whether the circuit breaker has tripped — runner should stop dispatching
|
|
1204
|
+
* new tasks and surface an `ImportThrottleAbortError` to the caller.
|
|
1205
|
+
*/
|
|
1206
|
+
shouldAbort() {
|
|
1207
|
+
return this.consecutiveThrottlesAtMin >= this.opts.maxConsecutiveThrottlesAtMin;
|
|
1208
|
+
}
|
|
1209
|
+
notify(reason, previous) {
|
|
1210
|
+
const cb = this.opts.onAdjust;
|
|
1211
|
+
if (!cb) return;
|
|
1212
|
+
try {
|
|
1213
|
+
cb({
|
|
1214
|
+
concurrency: this.current,
|
|
1215
|
+
previousConcurrency: previous,
|
|
1216
|
+
reason,
|
|
1217
|
+
tasksCompleted: this.completed,
|
|
1218
|
+
throttledCount: this.throttled
|
|
1219
|
+
});
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
async function runTaskWithUsage(fn) {
|
|
1225
|
+
const parent = usageScope.getStore();
|
|
1226
|
+
const taskAcc = { ru: 0, calls: 0, retries: 0 };
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await usageScope.run(taskAcc, fn);
|
|
1229
|
+
return { result, retries: taskAcc.retries };
|
|
1230
|
+
} finally {
|
|
1231
|
+
if (parent) {
|
|
1232
|
+
parent.ru += taskAcc.ru;
|
|
1233
|
+
parent.calls += taskAcc.calls;
|
|
1234
|
+
parent.retries += taskAcc.retries;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function sleep2(ms) {
|
|
1239
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1240
|
+
}
|
|
1241
|
+
async function runAdaptive(items, controller, fn) {
|
|
1242
|
+
if (items.length === 0) return [];
|
|
1243
|
+
controller.emitStartIfNeeded();
|
|
1117
1244
|
const results = new Array(items.length);
|
|
1118
1245
|
let nextIndex = 0;
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1246
|
+
let inFlight = 0;
|
|
1247
|
+
let aborted = false;
|
|
1248
|
+
let gate = createGate();
|
|
1249
|
+
function pokeGate() {
|
|
1250
|
+
const old = gate;
|
|
1251
|
+
gate = createGate();
|
|
1252
|
+
old.resolve();
|
|
1253
|
+
}
|
|
1254
|
+
async function worker() {
|
|
1255
|
+
while (nextIndex < items.length) {
|
|
1256
|
+
if (aborted) return;
|
|
1257
|
+
while (true) {
|
|
1258
|
+
if (aborted || nextIndex >= items.length) return;
|
|
1259
|
+
const cooldownRemaining = controller.getCooldownUntil() - Date.now();
|
|
1260
|
+
const slotsAvailable = inFlight < controller.getConcurrency();
|
|
1261
|
+
if (cooldownRemaining <= 0 && slotsAvailable) break;
|
|
1262
|
+
if (cooldownRemaining > 0) {
|
|
1263
|
+
await Promise.race([sleep2(cooldownRemaining), gate.promise]);
|
|
1264
|
+
} else {
|
|
1265
|
+
await gate.promise;
|
|
1126
1266
|
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1267
|
+
}
|
|
1268
|
+
if (aborted || nextIndex >= items.length) return;
|
|
1269
|
+
const idx = nextIndex++;
|
|
1270
|
+
inFlight++;
|
|
1271
|
+
try {
|
|
1272
|
+
const { result, retries } = await runTaskWithUsage(() => fn(items[idx]));
|
|
1273
|
+
results[idx] = result;
|
|
1274
|
+
if (retries > 0) {
|
|
1275
|
+
controller.noteThrottle();
|
|
1276
|
+
} else {
|
|
1277
|
+
controller.noteSuccess();
|
|
1278
|
+
}
|
|
1279
|
+
if (controller.shouldAbort()) {
|
|
1280
|
+
aborted = true;
|
|
1281
|
+
}
|
|
1282
|
+
} finally {
|
|
1283
|
+
inFlight--;
|
|
1284
|
+
pokeGate();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const workerCount = Math.min(items.length, controller.getMaxConcurrency());
|
|
1289
|
+
const workers = [];
|
|
1290
|
+
for (let w = 0; w < workerCount; w++) {
|
|
1291
|
+
workers.push(worker());
|
|
1129
1292
|
}
|
|
1130
1293
|
await Promise.all(workers);
|
|
1294
|
+
if (aborted) {
|
|
1295
|
+
throw new ImportThrottleAbortError(
|
|
1296
|
+
controller.getConcurrency(),
|
|
1297
|
+
controller.getConsecutiveThrottlesAtMin(),
|
|
1298
|
+
controller.getCompleted(),
|
|
1299
|
+
controller.getThrottledCount()
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1131
1302
|
return results;
|
|
1132
1303
|
}
|
|
1304
|
+
function createGate() {
|
|
1305
|
+
let resolve;
|
|
1306
|
+
const promise = new Promise((r) => {
|
|
1307
|
+
resolve = r;
|
|
1308
|
+
});
|
|
1309
|
+
return { promise, resolve };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/queries/bulk.ts
|
|
1313
|
+
var EXPORT_BATCH_SIZE = 100;
|
|
1133
1314
|
async function* exportAll(conn, repositoryId) {
|
|
1134
1315
|
let sequence = 0;
|
|
1135
1316
|
let cursor = "";
|
|
@@ -1190,11 +1371,12 @@ async function importBulk(conn, repositoryId, data, options) {
|
|
|
1190
1371
|
let relationshipsImported = 0;
|
|
1191
1372
|
const errors = [];
|
|
1192
1373
|
const skipCheck = options?.skipExistenceCheck ?? false;
|
|
1374
|
+
const controller = new AdaptiveConcurrencyController(options?.adaptiveConcurrency);
|
|
1193
1375
|
for (const chunk of data) {
|
|
1194
1376
|
if (chunk.entities && chunk.entities.length > 0) {
|
|
1195
|
-
const results = await
|
|
1377
|
+
const results = await runAdaptive(
|
|
1196
1378
|
chunk.entities,
|
|
1197
|
-
|
|
1379
|
+
controller,
|
|
1198
1380
|
async (entity) => {
|
|
1199
1381
|
try {
|
|
1200
1382
|
if (skipCheck) {
|
|
@@ -1221,9 +1403,9 @@ async function importBulk(conn, repositoryId, data, options) {
|
|
|
1221
1403
|
}
|
|
1222
1404
|
}
|
|
1223
1405
|
if (chunk.relationships && chunk.relationships.length > 0) {
|
|
1224
|
-
const results = await
|
|
1406
|
+
const results = await runAdaptive(
|
|
1225
1407
|
chunk.relationships,
|
|
1226
|
-
|
|
1408
|
+
controller,
|
|
1227
1409
|
async (rel) => {
|
|
1228
1410
|
try {
|
|
1229
1411
|
if (skipCheck) {
|
|
@@ -1517,20 +1699,18 @@ var CosmosDbProvider = class {
|
|
|
1517
1699
|
}
|
|
1518
1700
|
async deleteRepository(repositoryId, onProgress) {
|
|
1519
1701
|
this.assertValidRepositoryId(repositoryId);
|
|
1520
|
-
const batchDelayMs = this.config.deleteBatchDelayMs ?? 0;
|
|
1521
1702
|
return this.track(
|
|
1522
1703
|
"deleteRepository",
|
|
1523
1704
|
repositoryId,
|
|
1524
|
-
() => deleteRepository(this.conn, repositoryId, onProgress
|
|
1705
|
+
() => deleteRepository(this.conn, repositoryId, onProgress)
|
|
1525
1706
|
);
|
|
1526
1707
|
}
|
|
1527
1708
|
async deleteAllContents(repositoryId, onProgress) {
|
|
1528
1709
|
this.assertValidRepositoryId(repositoryId);
|
|
1529
|
-
const batchDelayMs = this.config.deleteBatchDelayMs ?? 0;
|
|
1530
1710
|
return this.track(
|
|
1531
1711
|
"deleteAllContents",
|
|
1532
1712
|
repositoryId,
|
|
1533
|
-
() => deleteAllContents(this.conn, repositoryId, onProgress
|
|
1713
|
+
() => deleteAllContents(this.conn, repositoryId, onProgress)
|
|
1534
1714
|
);
|
|
1535
1715
|
}
|
|
1536
1716
|
async getRepositoryStats(repositoryId) {
|