@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.cjs CHANGED
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/CosmosDbProvider.ts
39
39
  var import_node_crypto = __toESM(require("crypto"), 1);
40
- var import_deep_memory5 = require("@utaba/deep-memory");
40
+ var import_deep_memory6 = require("@utaba/deep-memory");
41
41
 
42
42
  // src/CosmosDbConnection.ts
43
43
  var import_gremlin = __toESM(require("gremlin"), 1);
@@ -129,24 +129,11 @@ function isTransientError(err) {
129
129
  return false;
130
130
  }
131
131
  function getRetryAfterMs(err, attempt) {
132
- const exponentialMs = Math.min(500 * Math.pow(2, attempt), 1e4);
133
- const topLevel = err?.["retryAfterMs"];
134
- if (typeof topLevel === "number" && topLevel > 0) {
135
- return Math.max(topLevel, exponentialMs);
136
- }
137
- const attrs = err?.statusAttributes;
138
- let hintVal;
139
- if (attrs instanceof Map) {
140
- hintVal = attrs.get("x-ms-retry-after-ms") ?? attrs.get("x-ms-retry-after");
141
- } else if (typeof attrs === "object" && attrs !== null) {
142
- const record = attrs;
143
- hintVal = record["x-ms-retry-after-ms"] ?? record["x-ms-retry-after"];
132
+ const retryAfter = err?.["retryAfterMs"];
133
+ if (typeof retryAfter === "number" && retryAfter > 0) {
134
+ return retryAfter;
144
135
  }
145
- const hintMs = Number(hintVal);
146
- if (Number.isFinite(hintMs) && hintMs > 0) {
147
- return Math.max(hintMs, exponentialMs);
148
- }
149
- return exponentialMs;
136
+ return Math.min(500 * Math.pow(2, attempt), 1e4);
150
137
  }
151
138
  function extractRequestCharge(resultSet) {
152
139
  const attrs = resultSet.attributes;
@@ -430,11 +417,7 @@ async function updateRepository(conn, repositoryId, updates) {
430
417
  return await getRepository(conn, repositoryId);
431
418
  }
432
419
  var DELETE_BATCH_SIZE = 500;
433
- function sleep2(ms) {
434
- return new Promise((resolve) => setTimeout(resolve, ms));
435
- }
436
- async function deleteRepository(conn, repositoryId, onProgress, options) {
437
- const batchDelayMs = options?.batchDelayMs ?? 0;
420
+ async function deleteRepository(conn, repositoryId, onProgress) {
438
421
  const entityCountResult = await conn.submit(
439
422
  "g.V().has('repositoryId', rid).has('entityType').count()",
440
423
  { rid: repositoryId }
@@ -460,7 +443,6 @@ async function deleteRepository(conn, repositoryId, onProgress, options) {
460
443
  relationshipsDeleted = totalRelationships - remainingCount;
461
444
  await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
462
445
  if (remainingCount === 0) break;
463
- if (batchDelayMs > 0) await sleep2(batchDelayMs);
464
446
  }
465
447
  while (true) {
466
448
  await conn.submit(
@@ -475,11 +457,9 @@ async function deleteRepository(conn, repositoryId, onProgress, options) {
475
457
  entitiesDeleted = totalEntities - remainingCount;
476
458
  await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
477
459
  if (remainingCount === 0) break;
478
- if (batchDelayMs > 0) await sleep2(batchDelayMs);
479
460
  }
480
461
  }
481
- async function deleteAllContents(conn, repositoryId, onProgress, options) {
482
- const batchDelayMs = options?.batchDelayMs ?? 0;
462
+ async function deleteAllContents(conn, repositoryId, onProgress) {
483
463
  const entityCountResult = await conn.submit(
484
464
  "g.V().has('repositoryId', rid).has('entityType').count()",
485
465
  { rid: repositoryId }
@@ -505,7 +485,6 @@ async function deleteAllContents(conn, repositoryId, onProgress, options) {
505
485
  relationshipsDeleted = totalRelationships - remainingCount;
506
486
  await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
507
487
  if (remainingCount === 0) break;
508
- if (batchDelayMs > 0) await sleep2(batchDelayMs);
509
488
  }
510
489
  while (true) {
511
490
  await conn.submit(
@@ -520,7 +499,6 @@ async function deleteAllContents(conn, repositoryId, onProgress, options) {
520
499
  entitiesDeleted = totalEntities - remainingCount;
521
500
  await onProgress?.({ entitiesDeleted, relationshipsDeleted, totalEntities, totalRelationships });
522
501
  if (remainingCount === 0) break;
523
- if (batchDelayMs > 0) await sleep2(batchDelayMs);
524
502
  }
525
503
  return { deletedEntities: totalEntities, deletedRelationships: totalRelationships };
526
504
  }
@@ -1147,26 +1125,229 @@ function isInTimeRange(timestamp, timeRange) {
1147
1125
  return timestamp >= timeRange.from && timestamp <= timeRange.to;
1148
1126
  }
1149
1127
 
1150
- // src/queries/bulk.ts
1151
- var EXPORT_BATCH_SIZE = 100;
1152
- var IMPORT_CONCURRENCY = 20;
1153
- async function runWithConcurrency(items, concurrency, fn) {
1128
+ // src/queries/adaptive-import.ts
1129
+ var import_deep_memory5 = require("@utaba/deep-memory");
1130
+ var DEFAULT_MIN = 2;
1131
+ var DEFAULT_START = 5;
1132
+ var DEFAULT_MAX = 32;
1133
+ var DEFAULT_INCREASE_AFTER = 50;
1134
+ var DEFAULT_COOLDOWN_MS = 1e3;
1135
+ var DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN = 10;
1136
+ function resolveOptions(opts) {
1137
+ const min = Math.max(1, opts?.min ?? DEFAULT_MIN);
1138
+ const max = Math.max(min, opts?.max ?? DEFAULT_MAX);
1139
+ const start = Math.min(max, Math.max(min, opts?.start ?? DEFAULT_START));
1140
+ const increaseAfter = Math.max(1, opts?.increaseAfter ?? DEFAULT_INCREASE_AFTER);
1141
+ const cooldownMs = Math.max(0, opts?.cooldownMs ?? DEFAULT_COOLDOWN_MS);
1142
+ const maxConsecutiveThrottlesAtMin = Math.max(
1143
+ 1,
1144
+ opts?.maxConsecutiveThrottlesAtMin ?? DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN
1145
+ );
1146
+ return {
1147
+ min,
1148
+ start,
1149
+ max,
1150
+ increaseAfter,
1151
+ cooldownMs,
1152
+ maxConsecutiveThrottlesAtMin,
1153
+ onAdjust: opts?.onAdjust
1154
+ };
1155
+ }
1156
+ var AdaptiveConcurrencyController = class {
1157
+ opts;
1158
+ current;
1159
+ streak = 0;
1160
+ cooldownUntil = 0;
1161
+ completed = 0;
1162
+ throttled = 0;
1163
+ startEmitted = false;
1164
+ consecutiveThrottlesAtMin = 0;
1165
+ constructor(opts) {
1166
+ this.opts = resolveOptions(opts);
1167
+ this.current = this.opts.start;
1168
+ }
1169
+ /** Current target concurrency. */
1170
+ getConcurrency() {
1171
+ return this.current;
1172
+ }
1173
+ /** Configured ceiling — used by the runner to size its worker pool. */
1174
+ getMaxConcurrency() {
1175
+ return this.opts.max;
1176
+ }
1177
+ /** Total tasks that have been noted as completed (success or throttle). */
1178
+ getCompleted() {
1179
+ return this.completed;
1180
+ }
1181
+ /** Total tasks that observed at least one throttle. */
1182
+ getThrottledCount() {
1183
+ return this.throttled;
1184
+ }
1185
+ /**
1186
+ * Earliest time (ms since epoch) at which a new task may be dispatched. Zero
1187
+ * if there is no active cooldown.
1188
+ */
1189
+ getCooldownUntil() {
1190
+ return this.cooldownUntil;
1191
+ }
1192
+ /**
1193
+ * Emit the initial `start` event the first time the controller is queried
1194
+ * for adjustment events. Kept separate so construction has no side effects.
1195
+ */
1196
+ emitStartIfNeeded() {
1197
+ if (this.startEmitted) return;
1198
+ this.startEmitted = true;
1199
+ this.notify("start", this.current);
1200
+ }
1201
+ /** Record a throttle-free task completion. May trigger ramp-up. */
1202
+ noteSuccess() {
1203
+ this.completed++;
1204
+ this.streak++;
1205
+ this.consecutiveThrottlesAtMin = 0;
1206
+ if (this.streak >= this.opts.increaseAfter && this.current < this.opts.max) {
1207
+ const previous = this.current;
1208
+ this.current = Math.min(this.opts.max, this.current + 1);
1209
+ this.streak = 0;
1210
+ this.notify("ramp-up", previous);
1211
+ }
1212
+ }
1213
+ /** Record a task that observed at least one throttle. Halves concurrency. */
1214
+ noteThrottle(now = Date.now()) {
1215
+ this.completed++;
1216
+ this.throttled++;
1217
+ this.streak = 0;
1218
+ const previous = this.current;
1219
+ const next = Math.max(this.opts.min, Math.floor(this.current / 2));
1220
+ this.cooldownUntil = now + this.opts.cooldownMs;
1221
+ if (next !== previous) {
1222
+ this.current = next;
1223
+ this.notify("throttle", previous);
1224
+ }
1225
+ if (this.current === this.opts.min) {
1226
+ this.consecutiveThrottlesAtMin++;
1227
+ } else {
1228
+ this.consecutiveThrottlesAtMin = 0;
1229
+ }
1230
+ }
1231
+ /** Configured circuit-breaker threshold. */
1232
+ getMaxConsecutiveThrottlesAtMin() {
1233
+ return this.opts.maxConsecutiveThrottlesAtMin;
1234
+ }
1235
+ /** How many consecutive throttles have occurred at min. */
1236
+ getConsecutiveThrottlesAtMin() {
1237
+ return this.consecutiveThrottlesAtMin;
1238
+ }
1239
+ /**
1240
+ * Whether the circuit breaker has tripped — runner should stop dispatching
1241
+ * new tasks and surface an `ImportThrottleAbortError` to the caller.
1242
+ */
1243
+ shouldAbort() {
1244
+ return this.consecutiveThrottlesAtMin >= this.opts.maxConsecutiveThrottlesAtMin;
1245
+ }
1246
+ notify(reason, previous) {
1247
+ const cb = this.opts.onAdjust;
1248
+ if (!cb) return;
1249
+ try {
1250
+ cb({
1251
+ concurrency: this.current,
1252
+ previousConcurrency: previous,
1253
+ reason,
1254
+ tasksCompleted: this.completed,
1255
+ throttledCount: this.throttled
1256
+ });
1257
+ } catch {
1258
+ }
1259
+ }
1260
+ };
1261
+ async function runTaskWithUsage(fn) {
1262
+ const parent = usageScope.getStore();
1263
+ const taskAcc = { ru: 0, calls: 0, retries: 0 };
1264
+ try {
1265
+ const result = await usageScope.run(taskAcc, fn);
1266
+ return { result, retries: taskAcc.retries };
1267
+ } finally {
1268
+ if (parent) {
1269
+ parent.ru += taskAcc.ru;
1270
+ parent.calls += taskAcc.calls;
1271
+ parent.retries += taskAcc.retries;
1272
+ }
1273
+ }
1274
+ }
1275
+ function sleep2(ms) {
1276
+ return new Promise((resolve) => setTimeout(resolve, ms));
1277
+ }
1278
+ async function runAdaptive(items, controller, fn) {
1279
+ if (items.length === 0) return [];
1280
+ controller.emitStartIfNeeded();
1154
1281
  const results = new Array(items.length);
1155
1282
  let nextIndex = 0;
1156
- const workers = [];
1157
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1158
- workers.push(
1159
- (async () => {
1160
- while (nextIndex < items.length) {
1161
- const idx = nextIndex++;
1162
- results[idx] = await fn(items[idx]);
1283
+ let inFlight = 0;
1284
+ let aborted = false;
1285
+ let gate = createGate();
1286
+ function pokeGate() {
1287
+ const old = gate;
1288
+ gate = createGate();
1289
+ old.resolve();
1290
+ }
1291
+ async function worker() {
1292
+ while (nextIndex < items.length) {
1293
+ if (aborted) return;
1294
+ while (true) {
1295
+ if (aborted || nextIndex >= items.length) return;
1296
+ const cooldownRemaining = controller.getCooldownUntil() - Date.now();
1297
+ const slotsAvailable = inFlight < controller.getConcurrency();
1298
+ if (cooldownRemaining <= 0 && slotsAvailable) break;
1299
+ if (cooldownRemaining > 0) {
1300
+ await Promise.race([sleep2(cooldownRemaining), gate.promise]);
1301
+ } else {
1302
+ await gate.promise;
1163
1303
  }
1164
- })()
1165
- );
1304
+ }
1305
+ if (aborted || nextIndex >= items.length) return;
1306
+ const idx = nextIndex++;
1307
+ inFlight++;
1308
+ try {
1309
+ const { result, retries } = await runTaskWithUsage(() => fn(items[idx]));
1310
+ results[idx] = result;
1311
+ if (retries > 0) {
1312
+ controller.noteThrottle();
1313
+ } else {
1314
+ controller.noteSuccess();
1315
+ }
1316
+ if (controller.shouldAbort()) {
1317
+ aborted = true;
1318
+ }
1319
+ } finally {
1320
+ inFlight--;
1321
+ pokeGate();
1322
+ }
1323
+ }
1324
+ }
1325
+ const workerCount = Math.min(items.length, controller.getMaxConcurrency());
1326
+ const workers = [];
1327
+ for (let w = 0; w < workerCount; w++) {
1328
+ workers.push(worker());
1166
1329
  }
1167
1330
  await Promise.all(workers);
1331
+ if (aborted) {
1332
+ throw new import_deep_memory5.ImportThrottleAbortError(
1333
+ controller.getConcurrency(),
1334
+ controller.getConsecutiveThrottlesAtMin(),
1335
+ controller.getCompleted(),
1336
+ controller.getThrottledCount()
1337
+ );
1338
+ }
1168
1339
  return results;
1169
1340
  }
1341
+ function createGate() {
1342
+ let resolve;
1343
+ const promise = new Promise((r) => {
1344
+ resolve = r;
1345
+ });
1346
+ return { promise, resolve };
1347
+ }
1348
+
1349
+ // src/queries/bulk.ts
1350
+ var EXPORT_BATCH_SIZE = 100;
1170
1351
  async function* exportAll(conn, repositoryId) {
1171
1352
  let sequence = 0;
1172
1353
  let cursor = "";
@@ -1227,11 +1408,12 @@ async function importBulk(conn, repositoryId, data, options) {
1227
1408
  let relationshipsImported = 0;
1228
1409
  const errors = [];
1229
1410
  const skipCheck = options?.skipExistenceCheck ?? false;
1411
+ const controller = new AdaptiveConcurrencyController(options?.adaptiveConcurrency);
1230
1412
  for (const chunk of data) {
1231
1413
  if (chunk.entities && chunk.entities.length > 0) {
1232
- const results = await runWithConcurrency(
1414
+ const results = await runAdaptive(
1233
1415
  chunk.entities,
1234
- IMPORT_CONCURRENCY,
1416
+ controller,
1235
1417
  async (entity) => {
1236
1418
  try {
1237
1419
  if (skipCheck) {
@@ -1258,9 +1440,9 @@ async function importBulk(conn, repositoryId, data, options) {
1258
1440
  }
1259
1441
  }
1260
1442
  if (chunk.relationships && chunk.relationships.length > 0) {
1261
- const results = await runWithConcurrency(
1443
+ const results = await runAdaptive(
1262
1444
  chunk.relationships,
1263
- IMPORT_CONCURRENCY,
1445
+ controller,
1264
1446
  async (rel) => {
1265
1447
  try {
1266
1448
  if (skipCheck) {
@@ -1369,11 +1551,11 @@ var META_VERTEX_ID = "_meta:schema";
1369
1551
  var CosmosDbProvider = class {
1370
1552
  conn;
1371
1553
  config;
1372
- compiler = new import_deep_memory5.GremlinCompiler();
1554
+ compiler = new import_deep_memory6.GremlinCompiler();
1373
1555
  reportUsage;
1374
1556
  constructor(config) {
1375
1557
  this.config = config;
1376
- this.reportUsage = (0, import_deep_memory5.createSafeSink)(config.reportUsage);
1558
+ this.reportUsage = (0, import_deep_memory6.createSafeSink)(config.reportUsage);
1377
1559
  this.conn = new CosmosDbConnection({
1378
1560
  endpoint: config.endpoint,
1379
1561
  key: config.key,
@@ -1427,8 +1609,8 @@ var CosmosDbProvider = class {
1427
1609
  * ever reaching query construction.
1428
1610
  */
1429
1611
  assertValidRepositoryId(repositoryId) {
1430
- if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
1431
- throw new import_deep_memory5.InvalidInputError(
1612
+ if (!(0, import_deep_memory6.isValidUuid)(repositoryId)) {
1613
+ throw new import_deep_memory6.InvalidInputError(
1432
1614
  "repositoryId",
1433
1615
  `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1434
1616
  );
@@ -1508,7 +1690,7 @@ var CosmosDbProvider = class {
1508
1690
  schemaVersion: SCHEMA_VERSION
1509
1691
  };
1510
1692
  } catch (err) {
1511
- throw new import_deep_memory5.ProviderError(
1693
+ throw new import_deep_memory6.ProviderError(
1512
1694
  `Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
1513
1695
  "Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
1514
1696
  );
@@ -1554,20 +1736,18 @@ var CosmosDbProvider = class {
1554
1736
  }
1555
1737
  async deleteRepository(repositoryId, onProgress) {
1556
1738
  this.assertValidRepositoryId(repositoryId);
1557
- const batchDelayMs = this.config.deleteBatchDelayMs ?? 0;
1558
1739
  return this.track(
1559
1740
  "deleteRepository",
1560
1741
  repositoryId,
1561
- () => deleteRepository(this.conn, repositoryId, onProgress, { batchDelayMs })
1742
+ () => deleteRepository(this.conn, repositoryId, onProgress)
1562
1743
  );
1563
1744
  }
1564
1745
  async deleteAllContents(repositoryId, onProgress) {
1565
1746
  this.assertValidRepositoryId(repositoryId);
1566
- const batchDelayMs = this.config.deleteBatchDelayMs ?? 0;
1567
1747
  return this.track(
1568
1748
  "deleteAllContents",
1569
1749
  repositoryId,
1570
- () => deleteAllContents(this.conn, repositoryId, onProgress, { batchDelayMs })
1750
+ () => deleteAllContents(this.conn, repositoryId, onProgress)
1571
1751
  );
1572
1752
  }
1573
1753
  async getRepositoryStats(repositoryId) {
@@ -1921,7 +2101,7 @@ var CosmosDbProvider = class {
1921
2101
  const props = obj;
1922
2102
  if (props["entityType"]) {
1923
2103
  const stored = entityFromGremlin(props);
1924
- pathEntities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
2104
+ pathEntities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
1925
2105
  }
1926
2106
  }
1927
2107
  if (pathEntities.length > 0) {
@@ -1936,7 +2116,7 @@ var CosmosDbProvider = class {
1936
2116
  } else {
1937
2117
  for (const item of result.items) {
1938
2118
  const stored = entityFromGremlin(item);
1939
- entities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
2119
+ entities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
1940
2120
  }
1941
2121
  }
1942
2122
  const limit = spec.limit ?? 50;
@@ -1962,7 +2142,7 @@ var CosmosDbProvider = class {
1962
2142
  queryMetadata
1963
2143
  };
1964
2144
  } catch (err) {
1965
- throw new import_deep_memory5.ProviderError(
2145
+ throw new import_deep_memory6.ProviderError(
1966
2146
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
1967
2147
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
1968
2148
  );
@@ -2023,7 +2203,7 @@ var CosmosDbProvider = class {
2023
2203
  queryMetadata
2024
2204
  };
2025
2205
  } catch (err) {
2026
- throw new import_deep_memory5.ProviderError(
2206
+ throw new import_deep_memory6.ProviderError(
2027
2207
  `Native Gremlin query failed: ${err instanceof Error ? err.message : String(err)}`,
2028
2208
  "Verify the Gremlin query syntax is valid for CosmosDB."
2029
2209
  );
@@ -2093,7 +2273,7 @@ async function cosmosRestBatchDelete(restBase, key, database, container, partiti
2093
2273
  });
2094
2274
  if (response.status !== 200 && response.status !== 207) {
2095
2275
  const text = await response.text();
2096
- throw new import_deep_memory5.ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
2276
+ throw new import_deep_memory6.ProviderError(`CosmosDB batch delete failed (${response.status}): ${text}`);
2097
2277
  }
2098
2278
  }
2099
2279
  // Annotate the CommonJS export names for ESM import in node: