@utaba/deep-memory-storage-cosmosdb 0.9.2 → 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);
@@ -1125,26 +1125,229 @@ function isInTimeRange(timestamp, timeRange) {
1125
1125
  return timestamp >= timeRange.from && timestamp <= timeRange.to;
1126
1126
  }
1127
1127
 
1128
- // src/queries/bulk.ts
1129
- var EXPORT_BATCH_SIZE = 100;
1130
- var IMPORT_CONCURRENCY = 20;
1131
- 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();
1132
1281
  const results = new Array(items.length);
1133
1282
  let nextIndex = 0;
1134
- const workers = [];
1135
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1136
- workers.push(
1137
- (async () => {
1138
- while (nextIndex < items.length) {
1139
- const idx = nextIndex++;
1140
- 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;
1141
1303
  }
1142
- })()
1143
- );
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());
1144
1329
  }
1145
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
+ }
1146
1339
  return results;
1147
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;
1148
1351
  async function* exportAll(conn, repositoryId) {
1149
1352
  let sequence = 0;
1150
1353
  let cursor = "";
@@ -1205,11 +1408,12 @@ async function importBulk(conn, repositoryId, data, options) {
1205
1408
  let relationshipsImported = 0;
1206
1409
  const errors = [];
1207
1410
  const skipCheck = options?.skipExistenceCheck ?? false;
1411
+ const controller = new AdaptiveConcurrencyController(options?.adaptiveConcurrency);
1208
1412
  for (const chunk of data) {
1209
1413
  if (chunk.entities && chunk.entities.length > 0) {
1210
- const results = await runWithConcurrency(
1414
+ const results = await runAdaptive(
1211
1415
  chunk.entities,
1212
- IMPORT_CONCURRENCY,
1416
+ controller,
1213
1417
  async (entity) => {
1214
1418
  try {
1215
1419
  if (skipCheck) {
@@ -1236,9 +1440,9 @@ async function importBulk(conn, repositoryId, data, options) {
1236
1440
  }
1237
1441
  }
1238
1442
  if (chunk.relationships && chunk.relationships.length > 0) {
1239
- const results = await runWithConcurrency(
1443
+ const results = await runAdaptive(
1240
1444
  chunk.relationships,
1241
- IMPORT_CONCURRENCY,
1445
+ controller,
1242
1446
  async (rel) => {
1243
1447
  try {
1244
1448
  if (skipCheck) {
@@ -1347,11 +1551,11 @@ var META_VERTEX_ID = "_meta:schema";
1347
1551
  var CosmosDbProvider = class {
1348
1552
  conn;
1349
1553
  config;
1350
- compiler = new import_deep_memory5.GremlinCompiler();
1554
+ compiler = new import_deep_memory6.GremlinCompiler();
1351
1555
  reportUsage;
1352
1556
  constructor(config) {
1353
1557
  this.config = config;
1354
- this.reportUsage = (0, import_deep_memory5.createSafeSink)(config.reportUsage);
1558
+ this.reportUsage = (0, import_deep_memory6.createSafeSink)(config.reportUsage);
1355
1559
  this.conn = new CosmosDbConnection({
1356
1560
  endpoint: config.endpoint,
1357
1561
  key: config.key,
@@ -1405,8 +1609,8 @@ var CosmosDbProvider = class {
1405
1609
  * ever reaching query construction.
1406
1610
  */
1407
1611
  assertValidRepositoryId(repositoryId) {
1408
- if (!(0, import_deep_memory5.isValidUuid)(repositoryId)) {
1409
- throw new import_deep_memory5.InvalidInputError(
1612
+ if (!(0, import_deep_memory6.isValidUuid)(repositoryId)) {
1613
+ throw new import_deep_memory6.InvalidInputError(
1410
1614
  "repositoryId",
1411
1615
  `repositoryId must be a valid v4 UUID; got '${repositoryId}'`
1412
1616
  );
@@ -1486,7 +1690,7 @@ var CosmosDbProvider = class {
1486
1690
  schemaVersion: SCHEMA_VERSION
1487
1691
  };
1488
1692
  } catch (err) {
1489
- throw new import_deep_memory5.ProviderError(
1693
+ throw new import_deep_memory6.ProviderError(
1490
1694
  `Failed to ensure CosmosDB schema: ${err instanceof Error ? err.message : String(err)}`,
1491
1695
  "Verify the CosmosDB REST endpoint is accessible and the Gremlin endpoint is reachable."
1492
1696
  );
@@ -1897,7 +2101,7 @@ var CosmosDbProvider = class {
1897
2101
  const props = obj;
1898
2102
  if (props["entityType"]) {
1899
2103
  const stored = entityFromGremlin(props);
1900
- pathEntities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
2104
+ pathEntities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
1901
2105
  }
1902
2106
  }
1903
2107
  if (pathEntities.length > 0) {
@@ -1912,7 +2116,7 @@ var CosmosDbProvider = class {
1912
2116
  } else {
1913
2117
  for (const item of result.items) {
1914
2118
  const stored = entityFromGremlin(item);
1915
- entities.push((0, import_deep_memory5.projectEntity)(stored, detailLevel));
2119
+ entities.push((0, import_deep_memory6.projectEntity)(stored, detailLevel));
1916
2120
  }
1917
2121
  }
1918
2122
  const limit = spec.limit ?? 50;
@@ -1938,7 +2142,7 @@ var CosmosDbProvider = class {
1938
2142
  queryMetadata
1939
2143
  };
1940
2144
  } catch (err) {
1941
- throw new import_deep_memory5.ProviderError(
2145
+ throw new import_deep_memory6.ProviderError(
1942
2146
  `Gremlin traversal failed: ${err instanceof Error ? err.message : String(err)}`,
1943
2147
  "Check the traversal spec and ensure the CosmosDB connection is healthy."
1944
2148
  );
@@ -1999,7 +2203,7 @@ var CosmosDbProvider = class {
1999
2203
  queryMetadata
2000
2204
  };
2001
2205
  } catch (err) {
2002
- throw new import_deep_memory5.ProviderError(
2206
+ throw new import_deep_memory6.ProviderError(
2003
2207
  `Native Gremlin query failed: ${err instanceof Error ? err.message : String(err)}`,
2004
2208
  "Verify the Gremlin query syntax is valid for CosmosDB."
2005
2209
  );
@@ -2069,7 +2273,7 @@ async function cosmosRestBatchDelete(restBase, key, database, container, partiti
2069
2273
  });
2070
2274
  if (response.status !== 200 && response.status !== 207) {
2071
2275
  const text = await response.text();
2072
- 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}`);
2073
2277
  }
2074
2278
  }
2075
2279
  // Annotate the CommonJS export names for ESM import in node: