@madewithremy/admin 0.1.14 → 0.1.16

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/prod.js CHANGED
@@ -342,6 +342,62 @@ function statsForMethod(ctx, methodId, params = {}) {
342
342
  );
343
343
  }
344
344
 
345
+ // src/sleep.ts
346
+ function sleep(ms) {
347
+ return new Promise((r) => setTimeout(r, ms));
348
+ }
349
+
350
+ // src/poll.ts
351
+ async function pollUntil(read, isSettled, options) {
352
+ const start2 = Date.now();
353
+ let movedAt = start2;
354
+ let lastKey;
355
+ let transientSince = null;
356
+ while (true) {
357
+ let value;
358
+ try {
359
+ value = await read();
360
+ transientSince = null;
361
+ } catch (err) {
362
+ if (!isTransientError(err)) {
363
+ throw err;
364
+ }
365
+ transientSince ??= Date.now();
366
+ if (Date.now() - transientSince > TRANSIENT_GRACE_MS) {
367
+ throw err;
368
+ }
369
+ options.onProgress?.(
370
+ `retrying after a gateway error\u2026 (${elapsedSeconds(start2)}s)`
371
+ );
372
+ await sleep(options.pollMs);
373
+ continue;
374
+ }
375
+ if (isSettled(value)) {
376
+ return { settled: true, value };
377
+ }
378
+ if (options.progressKey) {
379
+ const key = options.progressKey(value);
380
+ if (key !== lastKey) {
381
+ lastKey = key;
382
+ movedAt = Date.now();
383
+ }
384
+ }
385
+ if (Date.now() - movedAt > options.timeoutMs) {
386
+ return { settled: false, value };
387
+ }
388
+ if (options.describe) {
389
+ options.onProgress?.(options.describe(value, start2));
390
+ }
391
+ await sleep(options.pollMs);
392
+ }
393
+ }
394
+ function elapsedSeconds(start2) {
395
+ return Math.round((Date.now() - start2) / 1e3);
396
+ }
397
+ function timedOut(timeoutMs, still) {
398
+ return `Timed out after ${Math.round(timeoutMs / 1e3)}s ${still}`;
399
+ }
400
+
345
401
  // src/output.ts
346
402
  var PRETTY = process.stdout.isTTY === true;
347
403
  function out(data) {
@@ -352,6 +408,31 @@ function progress(message) {
352
408
  process.stderr.write(`${message}
353
409
  `);
354
410
  }
411
+ function describeEta(eta, now = Date.now()) {
412
+ if (!eta) {
413
+ return null;
414
+ }
415
+ const minutes = Math.max(1, Math.round((Date.parse(eta) - now) / 6e4));
416
+ if (minutes < 90) {
417
+ return `about ${minutes} min left`;
418
+ }
419
+ const hours = Math.round(minutes / 60 * 10) / 10;
420
+ return `about ${hours} h left`;
421
+ }
422
+ function renderProgress(p, start2, extra) {
423
+ const count = p.total !== null ? `${p.done.toLocaleString()}/${p.total.toLocaleString()} ${p.unit}` : p.done > 0 ? `${p.done.toLocaleString()} ${p.unit}` : null;
424
+ const rate = p.ratePerMinute !== null && p.ratePerMinute > 0 ? `${Math.round(p.ratePerMinute).toLocaleString()}/min` : null;
425
+ const parts = [
426
+ `${p.phase}\u2026`,
427
+ count,
428
+ rate,
429
+ describeEta(p.etaAt),
430
+ extra ?? null,
431
+ p.stalled ? "no progress for 30 min" : null
432
+ ].filter((part) => Boolean(part));
433
+ const elapsed = start2 !== void 0 ? ` (${elapsedSeconds(start2)}s)` : "";
434
+ return `${parts.join(" \xB7 ")}${elapsed}`;
435
+ }
355
436
 
356
437
  // src/commands/requests.ts
357
438
  var requestsSpecs = {
@@ -866,11 +947,6 @@ import { execFileSync } from "child_process";
866
947
  // src/config.ts
867
948
  var WORKSPACE_DIR = process.env["WORKSPACE_DIR"] || "/home/vercel-sandbox/workspace";
868
949
 
869
- // src/sleep.ts
870
- function sleep(ms) {
871
- return new Promise((r) => setTimeout(r, ms));
872
- }
873
-
874
950
  // src/ops/releases.ts
875
951
  function list3(ctx, params = {}) {
876
952
  return call(
@@ -2874,50 +2950,6 @@ import path5 from "path";
2874
2950
 
2875
2951
  // src/ops/dataSources.ts
2876
2952
  import { createHash as createHash2 } from "crypto";
2877
-
2878
- // src/poll.ts
2879
- async function pollUntil(read, isSettled, options) {
2880
- const start2 = Date.now();
2881
- let transientSince = null;
2882
- while (true) {
2883
- let value;
2884
- try {
2885
- value = await read();
2886
- transientSince = null;
2887
- } catch (err) {
2888
- if (!isTransientError(err)) {
2889
- throw err;
2890
- }
2891
- transientSince ??= Date.now();
2892
- if (Date.now() - transientSince > TRANSIENT_GRACE_MS) {
2893
- throw err;
2894
- }
2895
- options.onProgress?.(
2896
- `retrying after a gateway error\u2026 (${elapsedSeconds(start2)}s)`
2897
- );
2898
- await sleep(options.pollMs);
2899
- continue;
2900
- }
2901
- if (isSettled(value)) {
2902
- return { settled: true, value };
2903
- }
2904
- if (Date.now() - start2 > options.timeoutMs) {
2905
- return { settled: false, value };
2906
- }
2907
- if (options.describe) {
2908
- options.onProgress?.(options.describe(value, start2));
2909
- }
2910
- await sleep(options.pollMs);
2911
- }
2912
- }
2913
- function elapsedSeconds(start2) {
2914
- return Math.round((Date.now() - start2) / 1e3);
2915
- }
2916
- function timedOut(timeoutMs, still) {
2917
- return `Timed out after ${Math.round(timeoutMs / 1e3)}s ${still}`;
2918
- }
2919
-
2920
- // src/ops/dataSources.ts
2921
2953
  var DEFAULT_WAIT_TIMEOUT_MS = 15 * 60 * 1e3;
2922
2954
  var POLL_MS2 = 3e3;
2923
2955
  function base(appId) {
@@ -2998,6 +3030,7 @@ async function waitForIngest(ctx, params) {
2998
3030
  timeoutMs,
2999
3031
  pollMs: POLL_MS2,
3000
3032
  describe: (tracked2, start2) => `ingesting\u2026 ${tracked2.length - pendingOf(tracked2).length}/${tracked2.length} done (${elapsedSeconds(start2)}s)`,
3033
+ progressKey: (tracked2) => pendingOf(tracked2).length,
3001
3034
  onProgress
3002
3035
  }
3003
3036
  );
@@ -3034,6 +3067,7 @@ async function waitForCandidate(ctx, params) {
3034
3067
  timeoutMs,
3035
3068
  pollMs: POLL_MS2,
3036
3069
  describe: (progress2, start2) => progress2 ? `rebuilding\u2026 ${progress2.done}/${progress2.total} done (${elapsedSeconds(start2)}s)` : `rebuilding\u2026 (${elapsedSeconds(start2)}s)`,
3070
+ progressKey: (progress2) => progress2?.processing ?? null,
3037
3071
  onProgress: params.onProgress
3038
3072
  }
3039
3073
  );
@@ -3186,7 +3220,10 @@ async function waitForMove(ctx, params) {
3186
3220
  {
3187
3221
  timeoutMs,
3188
3222
  pollMs: POLL_MS2,
3189
- describe: (source2, start2) => `moving\u2026 ${source2.migration.copied}/${source2.migration.total} documents (${elapsedSeconds(start2)}s)`,
3223
+ describe: (source2, start2) => renderProgress(source2.migration.progress, start2),
3224
+ // A corpus of any size moves as long as it keeps moving; the timeout
3225
+ // bounds silence.
3226
+ progressKey: (source2) => source2.migration?.copied ?? null,
3190
3227
  onProgress: params.onProgress
3191
3228
  }
3192
3229
  );
@@ -3354,6 +3391,13 @@ function resume(ctx, params) {
3354
3391
  controlBody(params)
3355
3392
  );
3356
3393
  }
3394
+ function retry(ctx, params) {
3395
+ return call(
3396
+ ctx,
3397
+ "POST",
3398
+ `${base2(ctx.appId)}/${encodeURIComponent(params.id)}/retry`
3399
+ );
3400
+ }
3357
3401
  function cancel(ctx, params) {
3358
3402
  return call(
3359
3403
  ctx,
@@ -3380,6 +3424,9 @@ async function settle(ctx, params, isSettled) {
3380
3424
  timeoutMs,
3381
3425
  pollMs: POLL_MS3,
3382
3426
  describe: describeProgress,
3427
+ // A load of any length is waited out while it moves; the timeout bounds
3428
+ // silence. Planning moves by objects listed, a run by documents.
3429
+ progressKey: (job2) => `${job2.state}:${job2.progress.done}`,
3383
3430
  onProgress: params.onProgress
3384
3431
  }
3385
3432
  );
@@ -3415,14 +3462,15 @@ async function settle(ctx, params, isSettled) {
3415
3462
  return { status: "done", job };
3416
3463
  }
3417
3464
  function describeProgress(job, start2) {
3418
- const elapsed = `(${elapsedSeconds(start2)}s)`;
3419
- if (job.state === "planning") {
3420
- return job.enumerationDone ? `planning\u2026 reading a sample of ${job.counts.objectsSeen.toLocaleString()} objects ${elapsed}` : `planning\u2026 ${job.counts.objectsSeen.toLocaleString()} objects counted ${elapsed}`;
3465
+ if (job.state === "planning" && job.enumerationDone) {
3466
+ return renderProgress(
3467
+ { ...job.progress, phase: "planning" },
3468
+ start2,
3469
+ `reading a sample of ${job.counts.objectsSeen.toLocaleString()} objects`
3470
+ );
3421
3471
  }
3422
- const total = job.plan?.projected.documents ?? job.counts.objectsSeen;
3423
- const processed = job.counts.documentsDone + job.counts.documentsSkipped + job.counts.documentsFailed;
3424
- const spend = `$${job.estimatedDollars.toFixed(2)}${job.budgetDollars !== null ? ` of $${job.budgetDollars.toFixed(2)}` : ""}`;
3425
- return `${job.state}\u2026 ${processed.toLocaleString()}/${total.toLocaleString()} documents \xB7 ${spend} ${elapsed}`;
3472
+ const spend = job.state === "planning" ? null : `$${job.estimatedDollars.toFixed(2)}${job.budgetDollars !== null ? ` of $${job.budgetDollars.toFixed(2)}` : ""}`;
3473
+ return renderProgress(job.progress, start2, spend);
3426
3474
  }
3427
3475
 
3428
3476
  // src/commands/_shared/dataSources.ts
@@ -3690,7 +3738,7 @@ function hydrate(ctx, params) {
3690
3738
  ctx,
3691
3739
  "POST",
3692
3740
  `${base4(ctx.appId)}/hydrate`,
3693
- { slug: params.slug, ...params.since ? { since: params.since } : {} }
3741
+ { slug: params.slug }
3694
3742
  );
3695
3743
  }
3696
3744
  function reindex(ctx, params) {
@@ -3716,7 +3764,10 @@ async function waitForHydration(ctx, params) {
3716
3764
  {
3717
3765
  timeoutMs,
3718
3766
  pollMs: POLL_MS4,
3719
- describe: (source2, start2) => `loading\u2026 ${source2.hydration.copied}/${source2.hydration.total} documents (${elapsedSeconds(start2)}s)`,
3767
+ describe: (source2, start2) => renderProgress(source2.hydration.progress, start2),
3768
+ // A reload of any size is waited out while it moves; the timeout
3769
+ // bounds silence.
3770
+ progressKey: (source2) => source2.hydration?.copied ?? null,
3720
3771
  onProgress: params.onProgress
3721
3772
  }
3722
3773
  );
@@ -3904,10 +3955,9 @@ async function waitForSet(ctx, params) {
3904
3955
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3905
3956
  var dataSourceEvalsSpecs = {
3906
3957
  "datasources hydrate": {
3907
- usage: "Usage: remy-admin datasources hydrate [--source <slug>] [--since <iso>] [--wait] [--timeout <sec>]",
3958
+ usage: "Usage: remy-admin datasources hydrate [--source <slug>] [--wait] [--timeout <sec>]",
3908
3959
  flags: {
3909
3960
  source: { type: "string" },
3910
- since: { type: "string" },
3911
3961
  wait: { type: "boolean" },
3912
3962
  timeout: { type: "string" }
3913
3963
  }
@@ -4078,14 +4128,7 @@ function selectorFromFlag(raw) {
4078
4128
  }
4079
4129
  async function hydrate2(ctx, a) {
4080
4130
  const slug = a.str("source") || DEFAULT_SOURCE;
4081
- const since = a.str("since");
4082
- if (since && Number.isNaN(Date.parse(since))) {
4083
- fatal("--since must be an ISO 8601 timestamp, e.g. 2026-09-08T15:17:00Z.");
4084
- }
4085
- const started = await hydrate(ctx, {
4086
- slug,
4087
- ...since ? { since: new Date(since).toISOString() } : {}
4088
- });
4131
+ const started = await hydrate(ctx, { slug });
4089
4132
  if (started.resident || !a.bool("wait")) {
4090
4133
  out({
4091
4134
  dataSource: slug,
@@ -4380,7 +4423,7 @@ var dataSourceEvalsHandlers = {
4380
4423
  };
4381
4424
  var dataSourceEvalsHelp = `
4382
4425
  Warming and sampling:
4383
- remy-admin datasources hydrate [--source <slug>] [--since <iso>] [--wait]
4426
+ remy-admin datasources hydrate [--source <slug>] [--wait]
4384
4427
  remy-admin datasources sample --source <slug> --size <n> [--as <slug>] [--filter <k=v,...|json>] [--stratify <key>] [--placement <id|shared>] [--wait]
4385
4428
 
4386
4429
  A large source whose index was evicted (the shared pool holds a working set,
@@ -4388,15 +4431,9 @@ Warming and sampling:
4388
4431
  background; small sources reload inside the search and never show it.
4389
4432
  \`hydrate\` starts that reload ahead of time \u2014 before a demo, say \u2014 and
4390
4433
  \`--wait\` blocks until the index is back. \`datasources list\` shows the
4391
- reload's progress under \`hydration\`.
4392
-
4393
- \`hydrate --since <iso>\` is a refill rather than a warm-up: it copies every
4394
- document built at or after that time into the index whether or not the index
4395
- is resident. Dedicated capacity that restored from a snapshot is missing
4396
- everything written after the snapshot was taken; the platform starts this
4397
- refill itself when it sees the restore, and \`infra logs\` shows it. Use the
4398
- flag for a restore that predates that, with the snapshot's time from the
4399
- resource log.
4434
+ reload's progress under \`hydration\`. The same reload is what dedicated
4435
+ capacity runs on its own after a lost node or a resume; \`infra logs\` shows
4436
+ it start, and the source's \`hydration\` shows it land.
4400
4437
 
4401
4438
  \`sample\` draws documents from a source into a new one that shares its
4402
4439
  exact pinned config, so what you measure on the sample says something about
@@ -4512,6 +4549,14 @@ var dataSourceJobsSpecs = {
4512
4549
  timeout: { type: "string" }
4513
4550
  }
4514
4551
  },
4552
+ "datasources jobs retry": {
4553
+ usage: "Usage: remy-admin datasources jobs retry <id> [--wait] [--timeout <sec>]",
4554
+ positionals: [{ name: "id", required: true }],
4555
+ flags: {
4556
+ wait: { type: "boolean" },
4557
+ timeout: { type: "string" }
4558
+ }
4559
+ },
4515
4560
  "datasources jobs cancel": {
4516
4561
  usage: "Usage: remy-admin datasources jobs cancel <id>",
4517
4562
  positionals: [{ name: "id", required: true }]
@@ -4620,6 +4665,15 @@ async function jobsResume(ctx, a) {
4620
4665
  }
4621
4666
  await waitAndReport(ctx, a, id, "job", { action: "resume" });
4622
4667
  }
4668
+ async function jobsRetry(ctx, a) {
4669
+ const id = a.req("id");
4670
+ const { job } = await retry(ctx, { id });
4671
+ if (!a.bool("wait")) {
4672
+ out({ job });
4673
+ return;
4674
+ }
4675
+ await waitAndReport(ctx, a, id, "job", { action: "retry" });
4676
+ }
4623
4677
  async function jobsCancel(ctx, a) {
4624
4678
  out(await cancel(ctx, { id: a.req("id") }));
4625
4679
  }
@@ -4630,6 +4684,7 @@ var dataSourceJobsHandlers = {
4630
4684
  "datasources jobs approve": jobsApprove,
4631
4685
  "datasources jobs pause": jobsPause,
4632
4686
  "datasources jobs resume": jobsResume,
4687
+ "datasources jobs retry": jobsRetry,
4633
4688
  "datasources jobs cancel": jobsCancel
4634
4689
  };
4635
4690
  var dataSourceJobsHelp = `
@@ -4640,6 +4695,7 @@ Bulk ingestion (jobs):
4640
4695
  remy-admin datasources jobs status <id> [--wait] [--timeout <sec>]
4641
4696
  remy-admin datasources jobs approve <id> [--wait] [--timeout <sec>]
4642
4697
  remy-admin datasources jobs pause|resume|cancel <id>
4698
+ remy-admin datasources jobs retry <id> [--wait] [--timeout <sec>]
4643
4699
 
4644
4700
  A job loads a whole corpus at once. It reads either every object under a
4645
4701
  prefix of one of the app's file stores (\`files put\` fills one; an app can
@@ -4677,6 +4733,14 @@ Bulk ingestion (jobs):
4677
4733
  from the checkpoint and retries failed batches; \`jobs cancel\` stops it and
4678
4734
  keeps what was indexed. Search works on the partial corpus throughout.
4679
4735
 
4736
+ A batch that fails five times stays failed and the job finishes without it,
4737
+ with its documents left mid-build; \`jobs status\` shows the batch's error.
4738
+ Once the cause is fixed (capacity that was restoring, a provider outage),
4739
+ \`jobs retry <id>\` puts the job back to running over just those batches and
4740
+ finishes it again. A source with documents mid-build refuses to move or
4741
+ re-vectorize (data_source_busy) until they finish, so retry is what clears
4742
+ that.
4743
+
4680
4744
  One bulk operation per source: a job refuses to start during a move or while
4681
4745
  a candidate version exists, and moves, re-vectorizes and deletes refuse while
4682
4746
  a job is in flight (data_source_busy). Interactive \`add\` keeps working.
@@ -7643,7 +7707,14 @@ async function waitForPhase(ctx, params) {
7643
7707
  {
7644
7708
  timeoutMs,
7645
7709
  pollMs: POLL_MS5,
7646
- describe: (resource2, start2) => `${resource2.phase}${resource2.detail ? ` \xB7 ${resource2.detail}` : ""}${resource2.resizingTo ? ` \xB7 resizing to ${resource2.resizingTo.label}` : ""}\u2026 (${elapsedSeconds(start2)}s)`,
7710
+ describe: (resource2, start2) => resource2.progress ? renderProgress(
7711
+ resource2.progress,
7712
+ start2,
7713
+ resource2.resizingTo ? `resizing to ${resource2.resizingTo.label}` : null
7714
+ ) : `${resource2.phase}${resource2.detail ? ` \xB7 ${resource2.detail}` : ""}${resource2.resizingTo ? ` \xB7 resizing to ${resource2.resizingTo.label}` : ""}\u2026 (${elapsedSeconds(start2)}s)`,
7715
+ // A rebuild after a resume runs as long as it must; the timeout bounds
7716
+ // silence, and a phase or step change counts as movement.
7717
+ progressKey: (resource2) => `${resource2.phase}:${resource2.progress?.done ?? ""}:${resource2.detail ?? ""}`,
7647
7718
  onProgress: params.onProgress
7648
7719
  }
7649
7720
  );
@@ -7929,26 +8000,29 @@ Placing a data source on a resource:
7929
8000
 
7930
8001
  Searches on a source whose resource is not active fail with
7931
8002
  capacity_<phase>: capacity_hibernated once parked, capacity_hibernating /
7932
- capacity_resuming while it moves. \`active\` means searchable: a resumed
7933
- resource reports active only once its data is restored. capacity_restoring
7934
- is the rare case of an active instance that was replaced (node loss) and is
7935
- refilling from its snapshot. The messages are written for the app's end
7936
- user; the stable code is what to act on.
8003
+ capacity_resuming while it moves. Once active, a source whose index is being
8004
+ rebuilt from durable storage (after a resume, a resize or a lost node)
8005
+ answers index_warming with the rebuild's progress and ETA until it lands;
8006
+ \`datasources list\` shows it under hydration and the resource under
8007
+ progress. The messages are written for the app's end user; the stable code
8008
+ is what to act on.
7937
8009
 
7938
8010
  Resizing:
7939
- A resize keeps the data. The resource is parked (a fresh snapshot), its spec
7940
- is swapped, and it comes back up restored from that snapshot, so searches on
7941
- its sources pause for the few minutes it takes. A hibernated resource swaps
7942
- in place and stays parked. Growing needs a month's credits at the new rate;
7943
- shrinking is refused below what the resource holds (resize_too_small).
7944
- \`infra get\` shows resizingTo until the swap has happened.
8011
+ A resize keeps the documents: the resource is parked, its spec is swapped,
8012
+ and it comes back up with its sources' indexes rebuilt from durable storage
8013
+ in the background (nothing is re-embedded). Searches on those sources answer
8014
+ index_warming until the rebuild lands. A hibernated resource swaps in place
8015
+ and stays parked. Growing needs a month's credits at the new rate; shrinking
8016
+ is refused below what the resource holds (resize_too_small). \`infra get\`
8017
+ shows resizingTo until the swap has happened.
7945
8018
 
7946
8019
  Phases and timing:
7947
8020
  requested \u2192 provisioning \u2192 active \u21C4 hibernated \u2192 destroyed, with hibernating /
7948
8021
  resuming / decommissioning in between and failed when a change cannot complete
7949
- (retried automatically; resume or hibernate to retry by hand). Each transition
7950
- takes about two minutes, more with more data: snapshots and restores scale
7951
- with the collection. --wait defaults to 600s; pass --timeout for a large one.
8022
+ (retried automatically; resume or hibernate to retry by hand). A transition
8023
+ takes a minute or two; the rebuild that follows a resume runs under its own
8024
+ progress with a measured ETA. --wait gives up only after --timeout (default
8025
+ 600s) WITHOUT movement, so a long rebuild is waited out while it moves.
7952
8026
 
7953
8027
  Billing and the log:
7954
8028
  \`infra get\` reports resource.billing: what the resource has actually been
@@ -7957,7 +8031,8 @@ Billing and the log:
7957
8031
 
7958
8032
  The log is the platform's narration of the resource, not the instance's
7959
8033
  stdout: each step a transition went through ("Waiting for capacity",
7960
- "Restoring collections 2/3"), each failed attempt, and each failure with the
8034
+ "Rebuilding archive v1 from its artifacts: 14,333,673 document(s)"), each
8035
+ failed attempt, and each failure with the
7961
8036
  pod's recent Kubernetes events and the warnings-and-errors tail of the
7962
8037
  instance log at that moment (line.data). \`infra get\` includes the newest
7963
8038
  200 lines; \`infra logs\` pages the rest and, with --follow, prints new lines
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madewithremy/admin",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Admin SDK + CLI for managing production Remy apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",