@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 +232 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +221 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
|
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/
|
|
1129
|
-
var
|
|
1130
|
-
var
|
|
1131
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
|
1414
|
+
const results = await runAdaptive(
|
|
1211
1415
|
chunk.entities,
|
|
1212
|
-
|
|
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
|
|
1443
|
+
const results = await runAdaptive(
|
|
1240
1444
|
chunk.relationships,
|
|
1241
|
-
|
|
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
|
|
1554
|
+
compiler = new import_deep_memory6.GremlinCompiler();
|
|
1351
1555
|
reportUsage;
|
|
1352
1556
|
constructor(config) {
|
|
1353
1557
|
this.config = config;
|
|
1354
|
-
this.reportUsage = (0,
|
|
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,
|
|
1409
|
-
throw new
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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:
|