@utaba/deep-memory-storage-cosmosdb 0.10.0 → 0.11.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.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 exponentialMs = Math.min(500 * Math.pow(2, attempt), 1e4);
96
- const topLevel = err?.["retryAfterMs"];
97
- if (typeof topLevel === "number" && topLevel > 0) {
98
- return Math.max(topLevel, exponentialMs);
95
+ const retryAfter = err?.["retryAfterMs"];
96
+ if (typeof retryAfter === "number" && retryAfter > 0) {
97
+ return retryAfter;
99
98
  }
100
- const attrs = err?.statusAttributes;
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 sleep2(ms) {
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, options) {
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/bulk.ts
1114
- var EXPORT_BATCH_SIZE = 100;
1115
- var IMPORT_CONCURRENCY = 20;
1116
- async function runWithConcurrency(items, concurrency, fn) {
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
- const workers = [];
1120
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1121
- workers.push(
1122
- (async () => {
1123
- while (nextIndex < items.length) {
1124
- const idx = nextIndex++;
1125
- results[idx] = await fn(items[idx]);
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 runWithConcurrency(
1377
+ const results = await runAdaptive(
1196
1378
  chunk.entities,
1197
- IMPORT_CONCURRENCY,
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 runWithConcurrency(
1406
+ const results = await runAdaptive(
1225
1407
  chunk.relationships,
1226
- IMPORT_CONCURRENCY,
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, { batchDelayMs })
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, { batchDelayMs })
1713
+ () => deleteAllContents(this.conn, repositoryId, onProgress)
1534
1714
  );
1535
1715
  }
1536
1716
  async getRepositoryStats(repositoryId) {