trajectories-sh 1.10.3 → 1.12.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/cli.js CHANGED
@@ -81,7 +81,7 @@ async function uploadTrajectoryAction(directory, opts) {
81
81
  Uploading trajectory ${directory} \u2192 ${chalk.cyan(slug)}
82
82
  `));
83
83
  try {
84
- const { uploadJob } = await import("./upload-UXM6LEBL.js");
84
+ const { uploadJob } = await import("./upload-CDNIKYW3.js");
85
85
  const result = await uploadJob(directory, {
86
86
  slug,
87
87
  name: opts.name,
@@ -92,8 +92,11 @@ async function uploadTrajectoryAction(directory, opts) {
92
92
  if (result.meanReward != null) {
93
93
  console.log(` Pass rate: ${(result.meanReward * 100).toFixed(0)}%`);
94
94
  }
95
- console.log(` Viewer: ${result.viewerUrl}
96
- `);
95
+ console.log(` Viewer: ${result.viewerUrl}`);
96
+ if (result.existingDuplicateUrl) {
97
+ console.log(` Earlier copy: ${result.existingDuplicateUrl}`);
98
+ }
99
+ console.log("");
97
100
  } catch (e) {
98
101
  console.error(chalk.red(`
99
102
  \u2717 ${e.message}
@@ -10,8 +10,10 @@ import { tmpdir } from "os";
10
10
  import { randomBytes, createHash } from "crypto";
11
11
  var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
12
12
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
13
- var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "15");
13
+ var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "32");
14
14
  var BATCH_SIZE = 100;
15
+ var WEBP_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_WEBP_CONCURRENCY ?? "4");
16
+ var HASH_CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_HASH_CONCURRENCY ?? "16");
15
17
  function collectFiles(dir) {
16
18
  const files = [];
17
19
  function walk(current) {
@@ -85,26 +87,30 @@ async function convertToWebP(dir) {
85
87
  sharp.cache({ memory: 0, files: 0, items: 0 });
86
88
  let converted = 0;
87
89
  let savedBytes = 0;
88
- try {
89
- for (let i = 0; i < pngs.length; i++) {
90
- const f = pngs[i];
91
- const srcPath = join(tmpDir, f.relPath);
92
- const webpPath = srcPath.replace(/\.png$/i, ".webp");
93
- try {
94
- await pngToWebp(srcPath, webpPath);
95
- const origSize = statSync(srcPath).size;
96
- const newSize = statSync(webpPath).size;
97
- if (newSize < origSize) {
98
- unlinkSync(srcPath);
99
- savedBytes += origSize - newSize;
100
- converted++;
101
- } else {
102
- unlinkSync(webpPath);
103
- }
104
- } catch {
90
+ let processed = 0;
91
+ const total = pngs.length;
92
+ async function convertOne(f) {
93
+ const srcPath = join(tmpDir, f.relPath);
94
+ const webpPath = srcPath.replace(/\.png$/i, ".webp");
95
+ try {
96
+ await pngToWebp(srcPath, webpPath);
97
+ const origSize = statSync(srcPath).size;
98
+ const newSize = statSync(webpPath).size;
99
+ if (newSize < origSize) {
100
+ unlinkSync(srcPath);
101
+ savedBytes += origSize - newSize;
102
+ converted++;
103
+ } else {
104
+ unlinkSync(webpPath);
105
105
  }
106
- process.stdout.write(` Converting: ${i + 1}/${pngs.length} PNGs\r`);
106
+ } catch {
107
+ } finally {
108
+ processed++;
109
+ process.stdout.write(` Converting: ${processed}/${total} PNGs\r`);
107
110
  }
111
+ }
112
+ try {
113
+ await runWithConcurrency(pngs, WEBP_CONCURRENCY, convertOne);
108
114
  console.log("");
109
115
  } finally {
110
116
  try {
@@ -135,12 +141,34 @@ async function convertToWebP(dir) {
135
141
  }
136
142
  return { convertedDir: tmpDir, pngsConverted: converted, savedBytes };
137
143
  }
138
- function computeContentHash(files) {
139
- const hash = createHash("sha256");
144
+ async function runWithConcurrency(items, n, worker) {
145
+ if (items.length === 0) return;
146
+ let next = 0;
147
+ const runners = [];
148
+ for (let k = 0; k < Math.min(n, items.length); k++) {
149
+ runners.push(
150
+ (async () => {
151
+ while (true) {
152
+ const i = next++;
153
+ if (i >= items.length) return;
154
+ await worker(items[i], i);
155
+ }
156
+ })()
157
+ );
158
+ }
159
+ await Promise.all(runners);
160
+ }
161
+ async function computeContentHash(files) {
162
+ const { readFile } = await import("fs/promises");
140
163
  const sorted = [...files].sort((a, b) => a.relPath.localeCompare(b.relPath));
141
- for (const f of sorted) {
142
- hash.update(f.relPath);
143
- hash.update(readFileSync(f.absPath));
164
+ const contents = new Array(sorted.length);
165
+ await runWithConcurrency(sorted, HASH_CONCURRENCY, async (f, i) => {
166
+ contents[i] = await readFile(f.absPath);
167
+ });
168
+ const hash = createHash("sha256");
169
+ for (let i = 0; i < sorted.length; i++) {
170
+ hash.update(sorted[i].relPath);
171
+ hash.update(contents[i]);
144
172
  }
145
173
  return hash.digest("hex");
146
174
  }
@@ -280,9 +308,11 @@ async function uploadJob(dir, opts) {
280
308
  }
281
309
  const files = collectFiles(convertedDir);
282
310
  const totalSize = files.reduce((s, f) => s + f.size, 0);
283
- const contentHash = computeContentHash(files);
284
311
  console.log(` ${files.length} files, ${(totalSize / 1024 / 1024).toFixed(1)} MB`);
285
- console.log(` Hash: ${contentHash.slice(0, 16)}...`);
312
+ process.stdout.write(` Hashing...\r`);
313
+ const t0 = Date.now();
314
+ const contentHash = await computeContentHash(files);
315
+ console.log(` Hash: ${contentHash.slice(0, 16)}... (${((Date.now() - t0) / 1e3).toFixed(1)}s)`);
286
316
  try {
287
317
  const auth = await getAuthHeader();
288
318
  const usageResp = await fetch(`${getApiUrl()}/api/cli/usage`, {
@@ -315,14 +345,18 @@ async function uploadJob(dir, opts) {
315
345
  }
316
346
  const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility, estimated_bytes: totalSize, content_hash: contentHash });
317
347
  if (!initResp.ok) {
318
- if (initResp.status === 409) {
319
- const err = await initResp.json().catch(() => ({ detail: "Duplicate trajectory" }));
320
- throw new Error(err.detail);
321
- }
322
348
  throw new Error(`Init failed: ${await initResp.text()}`);
323
349
  }
324
- const { job_id: jobId } = await initResp.json();
350
+ const initBody = await initResp.json();
351
+ const jobId = initBody.job_id;
325
352
  console.log(` Job ID: ${jobId}`);
353
+ const apiUrlEarly = getApiUrl();
354
+ const siteUrlEarly = apiUrlEarly.includes("localhost") ? "http://localhost:3000" : apiUrlEarly.replace("api.", "").replace(/\/$/, "");
355
+ const existingDuplicateUrl = initBody.existing_duplicate ? `${siteUrlEarly}/t/${initBody.existing_duplicate.job_id}` : void 0;
356
+ if (existingDuplicateUrl) {
357
+ console.log(` \u24D8 This content was already uploaded as '${initBody.existing_duplicate.slug}' \u2014 uploading anyway.`);
358
+ console.log(` Existing: ${existingDuplicateUrl}`);
359
+ }
326
360
  await uploadDirect(jobId, files);
327
361
  console.log(" Finalizing...");
328
362
  const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
@@ -351,7 +385,8 @@ async function uploadJob(dir, opts) {
351
385
  slug: result.slug,
352
386
  nTrials: result.n_trials,
353
387
  meanReward: result.mean_reward,
354
- viewerUrl: `${siteUrl}/t/${jobId}`
388
+ viewerUrl: `${siteUrl}/t/${jobId}`,
389
+ existingDuplicateUrl
355
390
  };
356
391
  }
357
392
  async function uploadDirect(jobId, files) {
@@ -360,9 +395,16 @@ async function uploadDirect(jobId, files) {
360
395
  const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2191" });
361
396
  const failedFiles = [];
362
397
  const failureReasons = [];
363
- for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
364
- const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
365
- const urlMap = await fetchSignedUrlsForBatch(jobId, batch.map((f) => f.relPath));
398
+ const batches = [];
399
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
400
+ batches.push(files.slice(i, i + BATCH_SIZE));
401
+ }
402
+ let nextUrlsPromise = batches.length > 0 ? fetchSignedUrlsForBatch(jobId, batches[0].map((f) => f.relPath)) : null;
403
+ const pending = /* @__PURE__ */ new Set();
404
+ for (let bi = 0; bi < batches.length; bi++) {
405
+ const batch = batches[bi];
406
+ const urlMap = await nextUrlsPromise;
407
+ nextUrlsPromise = bi + 1 < batches.length ? fetchSignedUrlsForBatch(jobId, batches[bi + 1].map((f) => f.relPath)) : null;
366
408
  for (const file of batch) {
367
409
  if (!urlMap.has(file.relPath)) {
368
410
  failedFiles.push(file.relPath);
@@ -371,12 +413,9 @@ async function uploadDirect(jobId, files) {
371
413
  reason: "no signed URL from server after retries"
372
414
  });
373
415
  bar.tick(0);
416
+ continue;
374
417
  }
375
- }
376
- const pending = /* @__PURE__ */ new Set();
377
- for (const file of batch) {
378
418
  const signedUrl = urlMap.get(file.relPath);
379
- if (!signedUrl) continue;
380
419
  const task = (async () => {
381
420
  const result = await uploadOneFileWithError(file, signedUrl);
382
421
  if (result.ok) {
@@ -394,8 +433,8 @@ async function uploadDirect(jobId, files) {
394
433
  await Promise.race(pending);
395
434
  }
396
435
  }
397
- await Promise.all(pending);
398
436
  }
437
+ await Promise.all(pending);
399
438
  bar.done();
400
439
  if (failedFiles.length > 0) {
401
440
  const byReason = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.10.3",
3
+ "version": "1.12.0",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {