@le-space/ui 0.1.59 → 0.1.61

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/react/index.d.ts CHANGED
@@ -296,6 +296,7 @@ declare class SponsorRelayController {
296
296
  private stopWalletWatch;
297
297
  private props;
298
298
  private progressEmitter;
299
+ private runtimeCooldownByHash;
299
300
  constructor(props?: SponsorRelayProps);
300
301
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
301
302
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
@@ -303,6 +304,9 @@ declare class SponsorRelayController {
303
304
  private emit;
304
305
  private patch;
305
306
  private emitProgress;
307
+ private syncLatestDeploymentProgress;
308
+ private canSkipRuntimeRefresh;
309
+ private noteRuntimeRefreshResult;
306
310
  init(): Promise<void>;
307
311
  destroy(): void;
308
312
  setOpen(open: boolean): void;
package/react/index.js CHANGED
@@ -32,6 +32,10 @@ var IDLE_DEPLOYMENT_PROGRESS = {
32
32
  };
33
33
  var REFRESH_INTERVAL_MS = 3e4;
34
34
  var RELAY_PING_INTERVAL_MS = 2e4;
35
+ var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
36
+ var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
37
+ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
38
+ var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
35
39
 
36
40
  // src/shared/format.ts
37
41
  function shortHash(value, head = 8, tail = 6) {
@@ -1514,6 +1518,15 @@ async function inspectInstanceRuntime(args) {
1514
1518
  };
1515
1519
  }
1516
1520
  }
1521
+ function instanceTimestampMs(instance) {
1522
+ const value = instance.reception_time ?? instance.time;
1523
+ if (!value) return null;
1524
+ if (typeof value === "number") {
1525
+ return value > 0 && value < 1e10 ? value * 1e3 : value;
1526
+ }
1527
+ const parsed = Date.parse(value);
1528
+ return Number.isNaN(parsed) ? null : parsed;
1529
+ }
1517
1530
  function compatibleCrnsForTier(crns, state) {
1518
1531
  if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
1519
1532
  return [];
@@ -1575,6 +1588,9 @@ async function pingPeer(libp2p) {
1575
1588
  };
1576
1589
  }
1577
1590
  }
1591
+ function hasUsableRuntime(details) {
1592
+ return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1593
+ }
1578
1594
  var SponsorRelayController = class {
1579
1595
  state;
1580
1596
  subscribers = /* @__PURE__ */ new Set();
@@ -1584,6 +1600,7 @@ var SponsorRelayController = class {
1584
1600
  stopWalletWatch = null;
1585
1601
  props;
1586
1602
  progressEmitter = createDeploymentProgressEmitter();
1603
+ runtimeCooldownByHash = /* @__PURE__ */ new Map();
1587
1604
  constructor(props = {}) {
1588
1605
  this.props = props;
1589
1606
  this.state = defaultState(props);
@@ -1633,6 +1650,90 @@ var SponsorRelayController = class {
1633
1650
  });
1634
1651
  this.progressEmitter.emit(nextEvent);
1635
1652
  }
1653
+ syncLatestDeploymentProgress(instances) {
1654
+ const itemHash = this.state.lastDeploymentHash;
1655
+ if (!itemHash) {
1656
+ return;
1657
+ }
1658
+ const latest = instances.find(
1659
+ (entry) => entry.instance.item_hash === itemHash
1660
+ );
1661
+ if (!latest) {
1662
+ return;
1663
+ }
1664
+ const status = latest.details.messageStatus;
1665
+ if (status === "rejected") {
1666
+ this.emitProgress({
1667
+ stage: "deployment-rejected",
1668
+ label: "Deployment rejected",
1669
+ progress: 100,
1670
+ status: "error",
1671
+ itemHash,
1672
+ detail: "Aleph rejected the deployment.",
1673
+ error: "Aleph rejected the deployment."
1674
+ });
1675
+ return;
1676
+ }
1677
+ if (status !== "processed") {
1678
+ const submittedAtMs = instanceTimestampMs(latest.instance);
1679
+ const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
1680
+ this.emitProgress({
1681
+ stage: "waiting-for-aleph",
1682
+ label: pendingTooLong ? "Aleph processing delayed" : "Waiting for Aleph",
1683
+ progress: 76,
1684
+ status: pendingTooLong ? "warning" : "info",
1685
+ itemHash,
1686
+ detail: pendingTooLong ? "The instance is still pending on Aleph after several minutes. Retry, inspect the Aleph message, or delete and redeploy." : "Deployment submitted. Waiting for Aleph to process the instance message."
1687
+ });
1688
+ return;
1689
+ }
1690
+ if (!hasUsableRuntime(latest.details)) {
1691
+ this.emitProgress({
1692
+ stage: "deployment-confirmed",
1693
+ label: "Waiting for runtime",
1694
+ progress: 88,
1695
+ status: "warning",
1696
+ itemHash,
1697
+ detail: "Aleph processed the deployment. Waiting for scheduler/runtime allocation details."
1698
+ });
1699
+ return;
1700
+ }
1701
+ this.emitProgress({
1702
+ stage: "completed",
1703
+ label: "Runtime ready",
1704
+ progress: 100,
1705
+ status: "success",
1706
+ itemHash,
1707
+ detail: "Deployment processed and runtime networking is available."
1708
+ });
1709
+ }
1710
+ canSkipRuntimeRefresh(instance) {
1711
+ if (instance.item_hash === this.state.lastDeploymentHash) {
1712
+ return false;
1713
+ }
1714
+ const cooldownUntil = this.runtimeCooldownByHash.get(instance.item_hash);
1715
+ if (!cooldownUntil) {
1716
+ return false;
1717
+ }
1718
+ return cooldownUntil > Date.now();
1719
+ }
1720
+ noteRuntimeRefreshResult(instance, details) {
1721
+ const hasRuntimeData = Boolean(details.crnUrl) || Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1722
+ if (hasRuntimeData || details.error || details.messageStatus !== "processed") {
1723
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1724
+ return;
1725
+ }
1726
+ const timestampMs = instanceTimestampMs(instance);
1727
+ const isRecent = timestampMs != null && Date.now() - timestampMs < RECENT_INSTANCE_RUNTIME_GRACE_MS;
1728
+ if (isRecent) {
1729
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1730
+ return;
1731
+ }
1732
+ this.runtimeCooldownByHash.set(
1733
+ instance.item_hash,
1734
+ Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
1735
+ );
1736
+ }
1636
1737
  async init() {
1637
1738
  this.stopWalletWatch = watchWallet(() => {
1638
1739
  void this.refreshWalletDerivedState();
@@ -1752,25 +1853,11 @@ var SponsorRelayController = class {
1752
1853
  statusText: "Refreshing relay sponsor data"
1753
1854
  });
1754
1855
  try {
1755
- this.emitProgress({
1756
- stage: "loading-manifest",
1757
- label: "Loading manifest",
1758
- progress: 8,
1759
- status: "info",
1760
- detail: this.state.manifestUrl
1761
- });
1762
1856
  const manifestState = await resolveManifest({
1763
1857
  manifestUrl: this.state.manifestUrl,
1764
1858
  manifestJson: this.state.manifestJson
1765
1859
  });
1766
1860
  const manifest = manifestState.manifest;
1767
- this.emitProgress({
1768
- stage: "loading-pricing",
1769
- label: "Loading pricing and CRNs",
1770
- progress: 18,
1771
- status: "info",
1772
- detail: "Fetching live Aleph pricing and compatible CRNs."
1773
- });
1774
1861
  const [pricingSummary, crns] = await Promise.all([
1775
1862
  fetchInstancePricing(this.client.apiHost),
1776
1863
  this.client.fetchCrns()
@@ -1778,39 +1865,43 @@ var SponsorRelayController = class {
1778
1865
  let balance = this.state.balance;
1779
1866
  let instances = [];
1780
1867
  if (this.state.wallet.address) {
1781
- this.emitProgress({
1782
- stage: "loading-balance",
1783
- label: "Loading wallet balance",
1784
- progress: 28,
1785
- status: "info",
1786
- detail: this.state.wallet.address
1787
- });
1788
1868
  const [nextBalance, rawInstances] = await Promise.all([
1789
1869
  this.client.fetchBalance(this.state.wallet.address),
1790
1870
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
1791
1871
  ]);
1792
1872
  balance = nextBalance;
1793
1873
  instances = await Promise.all(
1794
- rawInstances.map(async (instance) => ({
1795
- instance,
1796
- details: await inspectInstanceRuntime({
1874
+ rawInstances.map(async (instance) => {
1875
+ const details = this.canSkipRuntimeRefresh(instance) ? {
1876
+ messageStatus: String(
1877
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
1878
+ ).toLowerCase(),
1879
+ allocationSource: null,
1880
+ crnUrl: null,
1881
+ hostIpv4: null,
1882
+ ipv6: null,
1883
+ vmIpv4: null,
1884
+ webUrl: null,
1885
+ sshCommand: null,
1886
+ mappedPorts: [],
1887
+ execution: null,
1888
+ error: null
1889
+ } : await inspectInstanceRuntime({
1797
1890
  client: this.client,
1798
1891
  instance,
1799
1892
  crns
1800
- })
1801
- }))
1893
+ });
1894
+ this.noteRuntimeRefreshResult(instance, details);
1895
+ return {
1896
+ instance,
1897
+ details
1898
+ };
1899
+ })
1802
1900
  );
1803
1901
  }
1804
1902
  let rootfsVerified = false;
1805
1903
  let rootfsResolution = null;
1806
1904
  if (manifestState.valid && manifest) {
1807
- this.emitProgress({
1808
- stage: "verifying-rootfs",
1809
- label: "Verifying rootfs",
1810
- progress: 38,
1811
- status: "info",
1812
- detail: manifest.rootfsItemHash
1813
- });
1814
1905
  rootfsVerified = await verifyRootfsExists(
1815
1906
  manifest.rootfsItemHash,
1816
1907
  this.client.apiHost
@@ -1841,26 +1932,13 @@ var SponsorRelayController = class {
1841
1932
  statusText: "Relay sponsor data ready"
1842
1933
  });
1843
1934
  this.recomputePricingSummary();
1844
- this.emitProgress({
1845
- stage: "completed",
1846
- label: "Data ready",
1847
- progress: 100,
1848
- status: "success",
1849
- detail: "Manifest, pricing, and wallet state are ready."
1850
- });
1935
+ this.syncLatestDeploymentProgress(instances);
1851
1936
  } catch (error) {
1852
1937
  this.patch({
1853
1938
  busy: { refreshing: false },
1854
1939
  errorText: error instanceof Error ? error.message : String(error),
1855
1940
  statusText: "Refresh failed"
1856
1941
  });
1857
- this.emitProgress({
1858
- stage: "error",
1859
- label: "Refresh failed",
1860
- progress: 100,
1861
- status: "error",
1862
- error: error instanceof Error ? error.message : String(error)
1863
- });
1864
1942
  }
1865
1943
  }
1866
1944
  async refreshRelayPing() {
@@ -1952,15 +2030,6 @@ var SponsorRelayController = class {
1952
2030
  detail: "Reloading deployments and runtime state."
1953
2031
  });
1954
2032
  await this.refresh();
1955
- this.emitProgress({
1956
- stage: "completed",
1957
- label: "Deployment completed",
1958
- progress: 100,
1959
- status: result.status === "rejected" ? "error" : "success",
1960
- itemHash: result.itemHash,
1961
- detail: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : "Deployment submitted and synced back into the panel.",
1962
- error: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : null
1963
- });
1964
2033
  } catch (error) {
1965
2034
  this.patch({
1966
2035
  busy: { deploying: false },
@@ -2132,6 +2201,20 @@ function progressBadgeLabel(stage) {
2132
2201
  return "DEPLOY";
2133
2202
  }
2134
2203
  }
2204
+ var COMPLETED_PROGRESS_VISIBLE_MS = 12e3;
2205
+ function isDeploymentProgressVisible(state) {
2206
+ const stage = state.deploymentProgress.stage;
2207
+ if (stage === "idle") {
2208
+ return false;
2209
+ }
2210
+ if (stage === "error") {
2211
+ return true;
2212
+ }
2213
+ if (stage === "completed") {
2214
+ return Date.now() - state.deploymentProgress.timestamp < COMPLETED_PROGRESS_VISIBLE_MS;
2215
+ }
2216
+ return true;
2217
+ }
2135
2218
  function launcherIndicator(state) {
2136
2219
  if (state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed") {
2137
2220
  return {
@@ -2140,7 +2223,7 @@ function launcherIndicator(state) {
2140
2223
  tone: state.deploymentProgress.status
2141
2224
  };
2142
2225
  }
2143
- if (state.deploymentProgress.stage === "completed") {
2226
+ if (state.deploymentProgress.stage === "completed" && isDeploymentProgressVisible(state)) {
2144
2227
  return {
2145
2228
  label: "DONE",
2146
2229
  detail: state.deploymentProgress.label,
@@ -2213,6 +2296,17 @@ function SponsorRelayFab(props) {
2213
2296
  state.deploymentProgress.status,
2214
2297
  state.deploymentProgress.timestamp
2215
2298
  ]);
2299
+ React.useEffect(() => {
2300
+ if (state.deploymentProgress.stage !== "completed") {
2301
+ return;
2302
+ }
2303
+ const timeout = window.setTimeout(() => {
2304
+ setSuccessFlash(false);
2305
+ }, COMPLETED_PROGRESS_VISIBLE_MS);
2306
+ return () => {
2307
+ window.clearTimeout(timeout);
2308
+ };
2309
+ }, [state.deploymentProgress.stage, state.deploymentProgress.timestamp]);
2216
2310
  React.useEffect(() => {
2217
2311
  if (launcherMode !== "inline" || !state.open) {
2218
2312
  return;
@@ -2258,6 +2352,7 @@ function SponsorRelayFab(props) {
2258
2352
  };
2259
2353
  }, [launcherMode]);
2260
2354
  const progressActive = state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed" && state.deploymentProgress.stage !== "error";
2355
+ const deploymentProgressVisible = isDeploymentProgressVisible(state);
2261
2356
  const pulseScale = progressActive ? 1.03 : hovered ? 1.015 : 1;
2262
2357
  const pulseShadow = progressActive ? `0 0 0 4px ${progressToneColor(indicator.tone)}22, 0 12px 28px rgba(15, 23, 42, 0.22)` : successFlash ? `0 0 0 5px rgba(55, 214, 122, 0.22), 0 14px 32px rgba(55, 214, 122, 0.22)` : launcherMode === "inline" ? hovered ? "0 14px 30px rgba(79, 70, 229, 0.28)" : "0 10px 24px rgba(99, 102, 241, 0.22)" : void 0;
2263
2358
  const inlineButtonBackground = indicator.tone === "error" ? "linear-gradient(135deg, #b91c1c 0%, #dc2626 100%)" : indicator.tone === "warning" ? "linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%)" : indicator.tone === "success" ? "linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)" : "linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)";
@@ -2437,7 +2532,7 @@ function SponsorRelayFab(props) {
2437
2532
  ),
2438
2533
  /* @__PURE__ */ jsx("p", { style: { color: "#9fb2ca" }, children: state.statusText }),
2439
2534
  state.errorText ? /* @__PURE__ */ jsx("p", { style: { color: "#ffd9d9" }, children: state.errorText }) : null,
2440
- state.deploymentProgress.stage !== "idle" ? /* @__PURE__ */ jsxs(
2535
+ deploymentProgressVisible ? /* @__PURE__ */ jsxs(
2441
2536
  "div",
2442
2537
  {
2443
2538
  style: {
package/shared/index.d.ts CHANGED
@@ -293,6 +293,12 @@ declare const RELAY_PING_IDLE_STATE: RelayPingState;
293
293
  declare const IDLE_DEPLOYMENT_PROGRESS: DeploymentProgressEvent;
294
294
  declare const REFRESH_INTERVAL_MS = 30000;
295
295
  declare const RELAY_PING_INTERVAL_MS = 20000;
296
+ declare const INSTANCE_RUNTIME_READY_REFRESH_MS = 30000;
297
+ declare const INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45000;
298
+ declare const INSTANCE_RUNTIME_STALE_REFRESH_MS: number;
299
+ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
300
+ declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
301
+ declare const DEPLOYMENT_PENDING_WARNING_MS: number;
296
302
 
297
303
  declare function shortHash(value: string | null | undefined, head?: number, tail?: number): string;
298
304
  declare function formatNumber(value: number | null | undefined, digits?: number): string;
@@ -328,6 +334,7 @@ declare class SponsorRelayController {
328
334
  private stopWalletWatch;
329
335
  private props;
330
336
  private progressEmitter;
337
+ private runtimeCooldownByHash;
331
338
  constructor(props?: SponsorRelayProps);
332
339
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
333
340
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
@@ -335,6 +342,9 @@ declare class SponsorRelayController {
335
342
  private emit;
336
343
  private patch;
337
344
  private emitProgress;
345
+ private syncLatestDeploymentProgress;
346
+ private canSkipRuntimeRefresh;
347
+ private noteRuntimeRefreshResult;
338
348
  init(): Promise<void>;
339
349
  destroy(): void;
340
350
  setOpen(open: boolean): void;
@@ -362,4 +372,4 @@ interface DeploymentProgressEmitter {
362
372
  }
363
373
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
364
374
 
365
- export { type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, REFRESH_INTERVAL_MS, RELAY_PING_IDLE_STATE, RELAY_PING_INTERVAL_MS, ROOTFS_MISSING_STATE, type RelayPingState, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, getEthereumProvider, joinMappedPorts, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
375
+ export { type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, DEPLOYMENT_PENDING_WARNING_MS, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, RELAY_PING_IDLE_STATE, RELAY_PING_INTERVAL_MS, ROOTFS_MISSING_STATE, type RelayPingState, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, getEthereumProvider, joinMappedPorts, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
package/shared/index.js CHANGED
@@ -29,6 +29,12 @@ var IDLE_DEPLOYMENT_PROGRESS = {
29
29
  };
30
30
  var REFRESH_INTERVAL_MS = 3e4;
31
31
  var RELAY_PING_INTERVAL_MS = 2e4;
32
+ var INSTANCE_RUNTIME_READY_REFRESH_MS = 3e4;
33
+ var INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45e3;
34
+ var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
35
+ var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
36
+ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
37
+ var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
32
38
 
33
39
  // src/shared/format.ts
34
40
  function shortHash(value, head = 8, tail = 6) {
@@ -1511,6 +1517,15 @@ async function inspectInstanceRuntime(args) {
1511
1517
  };
1512
1518
  }
1513
1519
  }
1520
+ function instanceTimestampMs(instance) {
1521
+ const value = instance.reception_time ?? instance.time;
1522
+ if (!value) return null;
1523
+ if (typeof value === "number") {
1524
+ return value > 0 && value < 1e10 ? value * 1e3 : value;
1525
+ }
1526
+ const parsed = Date.parse(value);
1527
+ return Number.isNaN(parsed) ? null : parsed;
1528
+ }
1514
1529
  function compatibleCrnsForTier(crns, state) {
1515
1530
  if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
1516
1531
  return [];
@@ -1572,6 +1587,9 @@ async function pingPeer(libp2p) {
1572
1587
  };
1573
1588
  }
1574
1589
  }
1590
+ function hasUsableRuntime(details) {
1591
+ return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1592
+ }
1575
1593
  var SponsorRelayController = class {
1576
1594
  state;
1577
1595
  subscribers = /* @__PURE__ */ new Set();
@@ -1581,6 +1599,7 @@ var SponsorRelayController = class {
1581
1599
  stopWalletWatch = null;
1582
1600
  props;
1583
1601
  progressEmitter = createDeploymentProgressEmitter();
1602
+ runtimeCooldownByHash = /* @__PURE__ */ new Map();
1584
1603
  constructor(props = {}) {
1585
1604
  this.props = props;
1586
1605
  this.state = defaultState(props);
@@ -1630,6 +1649,90 @@ var SponsorRelayController = class {
1630
1649
  });
1631
1650
  this.progressEmitter.emit(nextEvent);
1632
1651
  }
1652
+ syncLatestDeploymentProgress(instances) {
1653
+ const itemHash = this.state.lastDeploymentHash;
1654
+ if (!itemHash) {
1655
+ return;
1656
+ }
1657
+ const latest = instances.find(
1658
+ (entry) => entry.instance.item_hash === itemHash
1659
+ );
1660
+ if (!latest) {
1661
+ return;
1662
+ }
1663
+ const status = latest.details.messageStatus;
1664
+ if (status === "rejected") {
1665
+ this.emitProgress({
1666
+ stage: "deployment-rejected",
1667
+ label: "Deployment rejected",
1668
+ progress: 100,
1669
+ status: "error",
1670
+ itemHash,
1671
+ detail: "Aleph rejected the deployment.",
1672
+ error: "Aleph rejected the deployment."
1673
+ });
1674
+ return;
1675
+ }
1676
+ if (status !== "processed") {
1677
+ const submittedAtMs = instanceTimestampMs(latest.instance);
1678
+ const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
1679
+ this.emitProgress({
1680
+ stage: "waiting-for-aleph",
1681
+ label: pendingTooLong ? "Aleph processing delayed" : "Waiting for Aleph",
1682
+ progress: 76,
1683
+ status: pendingTooLong ? "warning" : "info",
1684
+ itemHash,
1685
+ detail: pendingTooLong ? "The instance is still pending on Aleph after several minutes. Retry, inspect the Aleph message, or delete and redeploy." : "Deployment submitted. Waiting for Aleph to process the instance message."
1686
+ });
1687
+ return;
1688
+ }
1689
+ if (!hasUsableRuntime(latest.details)) {
1690
+ this.emitProgress({
1691
+ stage: "deployment-confirmed",
1692
+ label: "Waiting for runtime",
1693
+ progress: 88,
1694
+ status: "warning",
1695
+ itemHash,
1696
+ detail: "Aleph processed the deployment. Waiting for scheduler/runtime allocation details."
1697
+ });
1698
+ return;
1699
+ }
1700
+ this.emitProgress({
1701
+ stage: "completed",
1702
+ label: "Runtime ready",
1703
+ progress: 100,
1704
+ status: "success",
1705
+ itemHash,
1706
+ detail: "Deployment processed and runtime networking is available."
1707
+ });
1708
+ }
1709
+ canSkipRuntimeRefresh(instance) {
1710
+ if (instance.item_hash === this.state.lastDeploymentHash) {
1711
+ return false;
1712
+ }
1713
+ const cooldownUntil = this.runtimeCooldownByHash.get(instance.item_hash);
1714
+ if (!cooldownUntil) {
1715
+ return false;
1716
+ }
1717
+ return cooldownUntil > Date.now();
1718
+ }
1719
+ noteRuntimeRefreshResult(instance, details) {
1720
+ const hasRuntimeData = Boolean(details.crnUrl) || Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1721
+ if (hasRuntimeData || details.error || details.messageStatus !== "processed") {
1722
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1723
+ return;
1724
+ }
1725
+ const timestampMs = instanceTimestampMs(instance);
1726
+ const isRecent = timestampMs != null && Date.now() - timestampMs < RECENT_INSTANCE_RUNTIME_GRACE_MS;
1727
+ if (isRecent) {
1728
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1729
+ return;
1730
+ }
1731
+ this.runtimeCooldownByHash.set(
1732
+ instance.item_hash,
1733
+ Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
1734
+ );
1735
+ }
1633
1736
  async init() {
1634
1737
  this.stopWalletWatch = watchWallet(() => {
1635
1738
  void this.refreshWalletDerivedState();
@@ -1749,25 +1852,11 @@ var SponsorRelayController = class {
1749
1852
  statusText: "Refreshing relay sponsor data"
1750
1853
  });
1751
1854
  try {
1752
- this.emitProgress({
1753
- stage: "loading-manifest",
1754
- label: "Loading manifest",
1755
- progress: 8,
1756
- status: "info",
1757
- detail: this.state.manifestUrl
1758
- });
1759
1855
  const manifestState = await resolveManifest({
1760
1856
  manifestUrl: this.state.manifestUrl,
1761
1857
  manifestJson: this.state.manifestJson
1762
1858
  });
1763
1859
  const manifest = manifestState.manifest;
1764
- this.emitProgress({
1765
- stage: "loading-pricing",
1766
- label: "Loading pricing and CRNs",
1767
- progress: 18,
1768
- status: "info",
1769
- detail: "Fetching live Aleph pricing and compatible CRNs."
1770
- });
1771
1860
  const [pricingSummary, crns] = await Promise.all([
1772
1861
  fetchInstancePricing(this.client.apiHost),
1773
1862
  this.client.fetchCrns()
@@ -1775,39 +1864,43 @@ var SponsorRelayController = class {
1775
1864
  let balance = this.state.balance;
1776
1865
  let instances = [];
1777
1866
  if (this.state.wallet.address) {
1778
- this.emitProgress({
1779
- stage: "loading-balance",
1780
- label: "Loading wallet balance",
1781
- progress: 28,
1782
- status: "info",
1783
- detail: this.state.wallet.address
1784
- });
1785
1867
  const [nextBalance, rawInstances] = await Promise.all([
1786
1868
  this.client.fetchBalance(this.state.wallet.address),
1787
1869
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
1788
1870
  ]);
1789
1871
  balance = nextBalance;
1790
1872
  instances = await Promise.all(
1791
- rawInstances.map(async (instance) => ({
1792
- instance,
1793
- details: await inspectInstanceRuntime({
1873
+ rawInstances.map(async (instance) => {
1874
+ const details = this.canSkipRuntimeRefresh(instance) ? {
1875
+ messageStatus: String(
1876
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
1877
+ ).toLowerCase(),
1878
+ allocationSource: null,
1879
+ crnUrl: null,
1880
+ hostIpv4: null,
1881
+ ipv6: null,
1882
+ vmIpv4: null,
1883
+ webUrl: null,
1884
+ sshCommand: null,
1885
+ mappedPorts: [],
1886
+ execution: null,
1887
+ error: null
1888
+ } : await inspectInstanceRuntime({
1794
1889
  client: this.client,
1795
1890
  instance,
1796
1891
  crns
1797
- })
1798
- }))
1892
+ });
1893
+ this.noteRuntimeRefreshResult(instance, details);
1894
+ return {
1895
+ instance,
1896
+ details
1897
+ };
1898
+ })
1799
1899
  );
1800
1900
  }
1801
1901
  let rootfsVerified = false;
1802
1902
  let rootfsResolution = null;
1803
1903
  if (manifestState.valid && manifest) {
1804
- this.emitProgress({
1805
- stage: "verifying-rootfs",
1806
- label: "Verifying rootfs",
1807
- progress: 38,
1808
- status: "info",
1809
- detail: manifest.rootfsItemHash
1810
- });
1811
1904
  rootfsVerified = await verifyRootfsExists(
1812
1905
  manifest.rootfsItemHash,
1813
1906
  this.client.apiHost
@@ -1838,26 +1931,13 @@ var SponsorRelayController = class {
1838
1931
  statusText: "Relay sponsor data ready"
1839
1932
  });
1840
1933
  this.recomputePricingSummary();
1841
- this.emitProgress({
1842
- stage: "completed",
1843
- label: "Data ready",
1844
- progress: 100,
1845
- status: "success",
1846
- detail: "Manifest, pricing, and wallet state are ready."
1847
- });
1934
+ this.syncLatestDeploymentProgress(instances);
1848
1935
  } catch (error) {
1849
1936
  this.patch({
1850
1937
  busy: { refreshing: false },
1851
1938
  errorText: error instanceof Error ? error.message : String(error),
1852
1939
  statusText: "Refresh failed"
1853
1940
  });
1854
- this.emitProgress({
1855
- stage: "error",
1856
- label: "Refresh failed",
1857
- progress: 100,
1858
- status: "error",
1859
- error: error instanceof Error ? error.message : String(error)
1860
- });
1861
1941
  }
1862
1942
  }
1863
1943
  async refreshRelayPing() {
@@ -1949,15 +2029,6 @@ var SponsorRelayController = class {
1949
2029
  detail: "Reloading deployments and runtime state."
1950
2030
  });
1951
2031
  await this.refresh();
1952
- this.emitProgress({
1953
- stage: "completed",
1954
- label: "Deployment completed",
1955
- progress: 100,
1956
- status: result.status === "rejected" ? "error" : "success",
1957
- itemHash: result.itemHash,
1958
- detail: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : "Deployment submitted and synced back into the panel.",
1959
- error: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : null
1960
- });
1961
2032
  } catch (error) {
1962
2033
  this.patch({
1963
2034
  busy: { deploying: false },
@@ -2046,11 +2117,17 @@ export {
2046
2117
  DEFAULT_INSTANCE_NAME,
2047
2118
  DEFAULT_MANIFEST_URL,
2048
2119
  DEFAULT_TIER_ID,
2120
+ DEPLOYMENT_PENDING_WARNING_MS,
2049
2121
  IDLE_DEPLOYMENT_PROGRESS,
2122
+ INSTANCE_RUNTIME_PENDING_REFRESH_MS,
2123
+ INSTANCE_RUNTIME_READY_REFRESH_MS,
2124
+ INSTANCE_RUNTIME_STALE_REFRESH_MS,
2125
+ RECENT_INSTANCE_RUNTIME_GRACE_MS,
2050
2126
  REFRESH_INTERVAL_MS,
2051
2127
  RELAY_PING_IDLE_STATE,
2052
2128
  RELAY_PING_INTERVAL_MS,
2053
2129
  ROOTFS_MISSING_STATE,
2130
+ STALE_INSTANCE_ALLOCATION_COOLDOWN_MS,
2054
2131
  SponsorRelayController,
2055
2132
  buildSshCommand,
2056
2133
  connectWallet,