@osfactory/har 0.40.0 → 0.41.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.js +141 -25
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31068,6 +31068,27 @@ function writePortalWatermark(repoPath, portalUrl, lastSyncedAt) {
|
|
|
31068
31068
|
}
|
|
31069
31069
|
writeState(state);
|
|
31070
31070
|
}
|
|
31071
|
+
function readRunsWatermarkEntry(repoPath, target) {
|
|
31072
|
+
const canonical = canonicalizeControlRepoPath(repoPath);
|
|
31073
|
+
const entry = readState().states.find(
|
|
31074
|
+
(state) => state.repoPath === canonical && state.portalUrl === target
|
|
31075
|
+
);
|
|
31076
|
+
return entry ? { lastSyncedAt: entry.lastSyncedAt, repoId: entry.repoId } : null;
|
|
31077
|
+
}
|
|
31078
|
+
function writeRunsWatermark(repoPath, target, repoId, lastSyncedAt) {
|
|
31079
|
+
const canonical = canonicalizeControlRepoPath(repoPath);
|
|
31080
|
+
const state = readState();
|
|
31081
|
+
const existing = state.states.find(
|
|
31082
|
+
(entry) => entry.repoPath === canonical && entry.portalUrl === target
|
|
31083
|
+
);
|
|
31084
|
+
if (existing) {
|
|
31085
|
+
existing.lastSyncedAt = lastSyncedAt;
|
|
31086
|
+
existing.repoId = repoId;
|
|
31087
|
+
} else {
|
|
31088
|
+
state.states.push({ repoPath: canonical, portalUrl: target, lastSyncedAt, repoId });
|
|
31089
|
+
}
|
|
31090
|
+
writeState(state);
|
|
31091
|
+
}
|
|
31071
31092
|
function selectSince(rows, since, getTimestamp) {
|
|
31072
31093
|
let maxSyncedAt = null;
|
|
31073
31094
|
const selected = [];
|
|
@@ -31275,6 +31296,7 @@ var init_portal_usage_merge = __esm({
|
|
|
31275
31296
|
// src/core/control-sync.ts
|
|
31276
31297
|
var control_sync_exports = {};
|
|
31277
31298
|
__export(control_sync_exports, {
|
|
31299
|
+
chunkBySerializedSize: () => chunkBySerializedSize,
|
|
31278
31300
|
discoverHarRepos: () => discoverHarRepos,
|
|
31279
31301
|
ensureRepoRegisteredWithControl: () => ensureRepoRegisteredWithControl,
|
|
31280
31302
|
isControlApiReachable: () => isControlApiReachable,
|
|
@@ -31286,13 +31308,80 @@ __export(control_sync_exports, {
|
|
|
31286
31308
|
syncRunWithControlAsync: () => syncRunWithControlAsync,
|
|
31287
31309
|
waitForControlApi: () => waitForControlApi
|
|
31288
31310
|
});
|
|
31311
|
+
function maxSyncBatchBytes() {
|
|
31312
|
+
const raw = Number(process.env.HAR_SYNC_MAX_BATCH_BYTES);
|
|
31313
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_BATCH_BYTES;
|
|
31314
|
+
}
|
|
31315
|
+
function chunkBySerializedSize(items, maxBytes) {
|
|
31316
|
+
const batches = [];
|
|
31317
|
+
let current = [];
|
|
31318
|
+
let currentBytes = 0;
|
|
31319
|
+
for (const item of items) {
|
|
31320
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
31321
|
+
if (current.length > 0 && currentBytes + itemBytes > maxBytes) {
|
|
31322
|
+
batches.push(current);
|
|
31323
|
+
current = [];
|
|
31324
|
+
currentBytes = 0;
|
|
31325
|
+
}
|
|
31326
|
+
current.push(item);
|
|
31327
|
+
currentBytes += itemBytes;
|
|
31328
|
+
}
|
|
31329
|
+
if (current.length > 0) batches.push(current);
|
|
31330
|
+
return batches;
|
|
31331
|
+
}
|
|
31332
|
+
function runTimestamp(run2) {
|
|
31333
|
+
return run2.finishedAt ?? run2.startedAt;
|
|
31334
|
+
}
|
|
31335
|
+
function rewindSince(since) {
|
|
31336
|
+
if (!since) return null;
|
|
31337
|
+
const raw = Number(process.env.HAR_SYNC_OVERLAP_MS);
|
|
31338
|
+
const overlap = Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_SYNC_OVERLAP_MS;
|
|
31339
|
+
return new Date(new Date(since).getTime() - overlap).toISOString();
|
|
31340
|
+
}
|
|
31341
|
+
function runsWatermarkTarget(target) {
|
|
31342
|
+
return `runs:${target}`;
|
|
31343
|
+
}
|
|
31344
|
+
async function syncRunBatches(newRuns, repoPath, runsTarget, getRepoId, dryRun, send) {
|
|
31345
|
+
const ordered = [...newRuns].sort((a2, b3) => runTimestamp(a2).localeCompare(runTimestamp(b3)));
|
|
31346
|
+
const batches = chunkBySerializedSize(ordered, maxSyncBatchBytes());
|
|
31347
|
+
const toSend = batches.length > 0 ? batches : [[]];
|
|
31348
|
+
for (let i2 = 0; i2 < toSend.length; i2++) {
|
|
31349
|
+
const batch = toSend[i2];
|
|
31350
|
+
await send(batch, i2);
|
|
31351
|
+
if (!dryRun && batch.length > 0) {
|
|
31352
|
+
writeRunsWatermark(repoPath, runsTarget, getRepoId(), runTimestamp(batch[batch.length - 1]));
|
|
31353
|
+
}
|
|
31354
|
+
}
|
|
31355
|
+
}
|
|
31356
|
+
function describeFetchFailure(url, bodyBytes, err) {
|
|
31357
|
+
const base = err instanceof Error ? err.message : String(err);
|
|
31358
|
+
const cause = err.cause;
|
|
31359
|
+
const code = cause?.code ? ` [${cause.code}]` : "";
|
|
31360
|
+
const detail = cause?.message && cause.message !== base ? `: ${cause.message}` : "";
|
|
31361
|
+
const size = bodyBytes > 0 ? ` (request body ${(bodyBytes / 1024 / 1024).toFixed(2)} MB)` : "";
|
|
31362
|
+
return `${base}${code}${detail}${size} \u2014 POST ${url}`;
|
|
31363
|
+
}
|
|
31364
|
+
async function postForResponse(url, headers, payload) {
|
|
31365
|
+
let lastErr;
|
|
31366
|
+
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
|
|
31367
|
+
try {
|
|
31368
|
+
return await fetch(url, { method: "POST", headers, body: payload });
|
|
31369
|
+
} catch (err) {
|
|
31370
|
+
lastErr = err;
|
|
31371
|
+
if (attempt < MAX_FETCH_ATTEMPTS) {
|
|
31372
|
+
await new Promise((resolve47) => setTimeout(resolve47, attempt * 100));
|
|
31373
|
+
}
|
|
31374
|
+
}
|
|
31375
|
+
}
|
|
31376
|
+
throw new Error(describeFetchFailure(url, Buffer.byteLength(payload, "utf8"), lastErr));
|
|
31377
|
+
}
|
|
31289
31378
|
async function postJson(url, body, dryRun) {
|
|
31290
31379
|
if (dryRun) return null;
|
|
31291
|
-
const response = await
|
|
31292
|
-
|
|
31293
|
-
|
|
31294
|
-
|
|
31295
|
-
|
|
31380
|
+
const response = await postForResponse(
|
|
31381
|
+
url,
|
|
31382
|
+
{ "Content-Type": "application/json" },
|
|
31383
|
+
JSON.stringify(body)
|
|
31384
|
+
);
|
|
31296
31385
|
if (!response.ok) {
|
|
31297
31386
|
const text = await response.text().catch(() => "");
|
|
31298
31387
|
throw new Error(`Control API ${response.status}: ${text || response.statusText}`);
|
|
@@ -31300,14 +31389,14 @@ async function postJson(url, body, dryRun) {
|
|
|
31300
31389
|
return await response.json();
|
|
31301
31390
|
}
|
|
31302
31391
|
function postPortalOnce(target, endpoint, body) {
|
|
31303
|
-
return
|
|
31304
|
-
|
|
31305
|
-
|
|
31392
|
+
return postForResponse(
|
|
31393
|
+
`${target.url}${endpoint}`,
|
|
31394
|
+
{
|
|
31306
31395
|
"Content-Type": "application/json",
|
|
31307
31396
|
Authorization: `Bearer ${target.token}`
|
|
31308
31397
|
},
|
|
31309
|
-
|
|
31310
|
-
|
|
31398
|
+
JSON.stringify(body)
|
|
31399
|
+
);
|
|
31311
31400
|
}
|
|
31312
31401
|
async function refreshPortalToken(target) {
|
|
31313
31402
|
if (!target.refreshToken) return false;
|
|
@@ -31331,7 +31420,7 @@ async function refreshPortalToken(target) {
|
|
|
31331
31420
|
return true;
|
|
31332
31421
|
}
|
|
31333
31422
|
async function postPortal(target, endpoint, body, dryRun) {
|
|
31334
|
-
if (dryRun) return;
|
|
31423
|
+
if (dryRun) return null;
|
|
31335
31424
|
let response = await postPortalOnce(target, endpoint, body);
|
|
31336
31425
|
if ((response.status === 401 || response.status === 403) && await refreshPortalToken(target)) {
|
|
31337
31426
|
response = await postPortalOnce(target, endpoint, body);
|
|
@@ -31345,6 +31434,7 @@ async function postPortal(target, endpoint, body, dryRun) {
|
|
|
31345
31434
|
const text = await response.text().catch(() => "");
|
|
31346
31435
|
throw new Error(`har-portal sync failed: HTTP ${response.status}${text ? ` \u2014 ${text}` : ""}`);
|
|
31347
31436
|
}
|
|
31437
|
+
return await response.json().catch(() => null);
|
|
31348
31438
|
}
|
|
31349
31439
|
function resolvePortalUserEmail() {
|
|
31350
31440
|
return readPortalCredentials()?.email || void 0;
|
|
@@ -31441,7 +31531,6 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
|
|
|
31441
31531
|
path: repoPath,
|
|
31442
31532
|
...manifest ? { manifest } : {},
|
|
31443
31533
|
...stagesRegistry ? { stagesRegistry } : {},
|
|
31444
|
-
runs,
|
|
31445
31534
|
slots: status.slots,
|
|
31446
31535
|
generatedAt: status.generatedAt,
|
|
31447
31536
|
...validations.length > 0 ? { validations } : {},
|
|
@@ -31450,7 +31539,7 @@ async function buildPortalPayload(repoPath, controlApiUrl, since) {
|
|
|
31450
31539
|
...validationBindings.length > 0 ? { validationBindings } : {},
|
|
31451
31540
|
...usage2.length > 0 ? { usage: usage2 } : {}
|
|
31452
31541
|
};
|
|
31453
|
-
return { syncBody, events, maxSyncedAt };
|
|
31542
|
+
return { syncBody, runs, events, maxSyncedAt };
|
|
31454
31543
|
}
|
|
31455
31544
|
async function isControlApiReachable(apiUrl = getControlApiUrl()) {
|
|
31456
31545
|
try {
|
|
@@ -31488,7 +31577,7 @@ async function syncReposWithControl(options) {
|
|
|
31488
31577
|
try {
|
|
31489
31578
|
await withTimeout(
|
|
31490
31579
|
syncRepoWithControl({ repoPath, apiUrl, dryRun: options.dryRun, cloud: options.cloud, full: options.full }),
|
|
31491
|
-
|
|
31580
|
+
EXPLICIT_SYNC_TIMEOUT_MS,
|
|
31492
31581
|
"control sync"
|
|
31493
31582
|
);
|
|
31494
31583
|
synced++;
|
|
@@ -31551,7 +31640,7 @@ async function ensureRepoRegisteredWithControl(repoPath, apiUrl) {
|
|
|
31551
31640
|
}
|
|
31552
31641
|
async function syncRepoWithLocalControl(options) {
|
|
31553
31642
|
const apiUrl = options.apiUrl ?? getControlApiUrl();
|
|
31554
|
-
const { repoPath, dryRun } = options;
|
|
31643
|
+
const { repoPath, dryRun, full } = options;
|
|
31555
31644
|
const registerResult = await registerRepoWithControl({ ...options, repoPath });
|
|
31556
31645
|
const repoId = registerResult?.id;
|
|
31557
31646
|
if (!repoId && !dryRun) {
|
|
@@ -31562,11 +31651,11 @@ async function syncRepoWithLocalControl(options) {
|
|
|
31562
31651
|
if (!existing) {
|
|
31563
31652
|
return;
|
|
31564
31653
|
}
|
|
31565
|
-
await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun);
|
|
31654
|
+
await syncRepoRunsAndSlots(apiUrl, existing.id, repoPath, dryRun, full);
|
|
31566
31655
|
return;
|
|
31567
31656
|
}
|
|
31568
31657
|
if (repoId) {
|
|
31569
|
-
await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun);
|
|
31658
|
+
await syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full);
|
|
31570
31659
|
}
|
|
31571
31660
|
}
|
|
31572
31661
|
async function syncRepoWithPortal(options, controlApiUrl) {
|
|
@@ -31574,10 +31663,28 @@ async function syncRepoWithPortal(options, controlApiUrl) {
|
|
|
31574
31663
|
if (!portal) return;
|
|
31575
31664
|
const { repoPath, dryRun, full } = options;
|
|
31576
31665
|
const since = full ? null : readPortalWatermark(repoPath, portal.url);
|
|
31577
|
-
const
|
|
31578
|
-
|
|
31579
|
-
|
|
31580
|
-
|
|
31666
|
+
const runsTarget = runsWatermarkTarget(portal.url);
|
|
31667
|
+
const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
|
|
31668
|
+
const runsSince = rewindSince(stored?.lastSyncedAt ?? null);
|
|
31669
|
+
const { syncBody, runs, events, maxSyncedAt } = await buildPortalPayload(
|
|
31670
|
+
repoPath,
|
|
31671
|
+
controlApiUrl,
|
|
31672
|
+
since
|
|
31673
|
+
);
|
|
31674
|
+
const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
|
|
31675
|
+
let repoId;
|
|
31676
|
+
await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch, i2) => {
|
|
31677
|
+
const body = i2 === 0 ? { ...syncBody, runs: batch } : { path: repoPath, runs: batch };
|
|
31678
|
+
const res = await postPortal(portal, "/api/sync", body, dryRun);
|
|
31679
|
+
if (i2 === 0 && typeof res?.repositoryId === "string") repoId = res.repositoryId;
|
|
31680
|
+
});
|
|
31681
|
+
if (!full && !dryRun && stored?.repoId && repoId && stored.repoId !== repoId) {
|
|
31682
|
+
await syncRunBatches(listRuns(repoPath), repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
|
|
31683
|
+
await postPortal(portal, "/api/sync", { path: repoPath, runs: batch }, dryRun);
|
|
31684
|
+
});
|
|
31685
|
+
}
|
|
31686
|
+
for (const batch of chunkBySerializedSize(events, maxSyncBatchBytes())) {
|
|
31687
|
+
await postPortal(portal, "/api/otel", { path: repoPath, events: batch, spans: [] }, dryRun);
|
|
31581
31688
|
}
|
|
31582
31689
|
if (!dryRun && maxSyncedAt) {
|
|
31583
31690
|
writePortalWatermark(repoPath, portal.url, maxSyncedAt);
|
|
@@ -31618,10 +31725,15 @@ async function syncRepoWithControl(options) {
|
|
|
31618
31725
|
await syncRepoWithLocalControl({ ...options, repoPath, apiUrl });
|
|
31619
31726
|
await syncRepoWithPortal({ ...options, repoPath }, apiUrl);
|
|
31620
31727
|
}
|
|
31621
|
-
async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun) {
|
|
31728
|
+
async function syncRepoRunsAndSlots(apiUrl, repoId, repoPath, dryRun, full) {
|
|
31729
|
+
const runsTarget = runsWatermarkTarget(apiUrl);
|
|
31730
|
+
const stored = full ? null : readRunsWatermarkEntry(repoPath, runsTarget);
|
|
31731
|
+
const runsSince = rewindSince(stored?.repoId === repoId ? stored?.lastSyncedAt ?? null : null);
|
|
31622
31732
|
const runs = listRuns(repoPath);
|
|
31623
|
-
const
|
|
31624
|
-
await
|
|
31733
|
+
const { selected: newRuns } = selectSince(runs, runsSince, runTimestamp);
|
|
31734
|
+
await syncRunBatches(newRuns, repoPath, runsTarget, () => repoId, dryRun, async (batch) => {
|
|
31735
|
+
await postJson(`${apiUrl}/api/repos/${repoId}/runs`, SyncRunsInputSchema.parse({ runs: batch }), dryRun);
|
|
31736
|
+
});
|
|
31625
31737
|
const status = collectEnvironmentStatus(repoPath);
|
|
31626
31738
|
const slotsBody = SyncSlotsInputSchema.parse({
|
|
31627
31739
|
slots: status.slots,
|
|
@@ -31776,7 +31888,7 @@ async function syncRunWithControlAsync(repoPath, run2) {
|
|
|
31776
31888
|
`);
|
|
31777
31889
|
}
|
|
31778
31890
|
}
|
|
31779
|
-
var path27, SYNC_TIMEOUT_MS;
|
|
31891
|
+
var path27, DEFAULT_MAX_BATCH_BYTES, DEFAULT_SYNC_OVERLAP_MS, MAX_FETCH_ATTEMPTS, SYNC_TIMEOUT_MS, EXPLICIT_SYNC_TIMEOUT_MS;
|
|
31780
31892
|
var init_control_sync = __esm({
|
|
31781
31893
|
"src/core/control-sync.ts"() {
|
|
31782
31894
|
"use strict";
|
|
@@ -31800,7 +31912,11 @@ var init_control_sync = __esm({
|
|
|
31800
31912
|
init_portal_usage_merge();
|
|
31801
31913
|
init_schema2();
|
|
31802
31914
|
init_portal_watermark();
|
|
31915
|
+
DEFAULT_MAX_BATCH_BYTES = 4 * 1024 * 1024;
|
|
31916
|
+
DEFAULT_SYNC_OVERLAP_MS = 6e4;
|
|
31917
|
+
MAX_FETCH_ATTEMPTS = 3;
|
|
31803
31918
|
SYNC_TIMEOUT_MS = 15e3;
|
|
31919
|
+
EXPLICIT_SYNC_TIMEOUT_MS = 12e4;
|
|
31804
31920
|
}
|
|
31805
31921
|
});
|
|
31806
31922
|
|