Ctrl K
ComunidadGuías…

src/services/updateReplicationToast.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { VNode } from 'vue'
import { h } from 'vue'
import { toast } from 'vue-sonner'
import { i18n } from '~/modules/i18n'

interface DeploymentRegion {
  code: string
  label: string
}

interface DeploymentToastContext {
  eventLabel: string
  route?: string | null
  actionLabel?: string
  onAction?: () => void
}

type Timer = ReturnType<typeof setInterval>
type Timeout = ReturnType<typeof setTimeout>

const DEPLOYMENT_REGIONS: DeploymentRegion[] = [
  { code: 'aws:eu-central-1', label: 'EU (Frankfurt)' },
  { code: 'gcp:me-central1', label: 'Middle East (Riyadh)' },
  { code: 'gcp:asia-east2', label: 'Hong Kong' },
  { code: 'aws:ap-northeast-1', label: 'Japan (Tokyo)' },
  { code: 'aws:ap-south-1', label: 'India (Mumbai)' },
  { code: 'aws:us-east-1', label: 'North America (Virginia)' },
  { code: 'gcp:africa-south1', label: 'Africa (South Africa)' },
  { code: 'aws:ap-southeast-2', label: 'Oceania (Sydney)' },
  { code: 'aws:sa-east-1', label: 'South America (São Paulo)' },
]

// Update distribution got faster: the walkthrough over all regions takes 30s.
const TOTAL_REPLICATION_MS = 30_000
const UPDATE_INTERVAL_MS = 500
const REPLICATION_TOAST_CLASSES = {
  content: 'w-full min-w-0',
  toast: 'replication-toast',
}
const REPLICATION_DONE_TOAST_STYLE = {
  '--initial-height': 'auto',
}

function getCurrentTimeZone(): string | null {
  if (typeof Intl === 'undefined')
    return null

  const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone
  return timeZone || null
}

function getClosestRegionFromTimeZone(): string | null {
  const tz = getCurrentTimeZone()
  if (!tz)
    return null

  if (tz.startsWith('America/'))
    return 'aws:us-east-1'
  if (tz.startsWith('Europe/') || tz.startsWith('Atlantic/'))
    return 'aws:eu-central-1'
  if (tz.startsWith('Africa/'))
    return 'gcp:africa-south1'

  if (tz.startsWith('Asia/')) {
    const region = (tz.split('/')[1] ?? '').toLowerCase()
    if (['tokyo', 'osaka', 'seoul', 'pyongyang'].includes(region))
      return 'aws:ap-northeast-1'
    if (['kolkata', 'mumbai', 'calcutta', 'chennai', 'bangalore', 'hyderabad', 'delhi', 'dhaka', 'colombo', 'karachi'].includes(region))
      return 'aws:ap-south-1'
    if (['hong_kong', 'singapore', 'manila', 'jakarta', 'kuala_lumpur', 'beijing', 'shanghai', 'hongkong', 'taipei'].includes(region))
      return 'gcp:asia-east2'
    if (['sydney', 'melbourne', 'canberra', 'perth', 'brisbane', 'darwin', 'adelaide'].includes(region))
      return 'aws:ap-southeast-2'
    return 'gcp:asia-east2'
  }

  if (tz.startsWith('Pacific/') || tz.startsWith('Australia/'))
    return 'aws:ap-southeast-2'

  if (typeof navigator !== 'undefined') {
    const language = navigator.language?.toLowerCase() || ''
    if (language.endsWith('-in'))
      return 'aws:ap-south-1'
    if (language.endsWith('-br') || language.endsWith('-ar'))
      return 'aws:sa-east-1'
    if (language.endsWith('-jp'))
      return 'aws:ap-northeast-1'
    if (language.endsWith('-za'))
      return 'gcp:africa-south1'
  }

  return null
}

function getOrderedRegions(): DeploymentRegion[] {
  const sorted = [...DEPLOYMENT_REGIONS]
  const closestCode = getClosestRegionFromTimeZone()
  if (!closestCode)
    return sorted

  const idx = sorted.findIndex(region => region.code === closestCode)
  if (idx < 0)
    return sorted

  const [closest] = sorted.splice(idx, 1)
  sorted.push(closest)
  return sorted
}

function formatDuration(seconds: number) {
  return i18n.global.t('replication-toast-time-left', { seconds })
}

function buildActionButton(actionLabel?: string, onAction?: () => void): VNode | null {
  if (!actionLabel || !onAction)
    return null

  return h('div', { class: 'mt-2 flex justify-center' }, [
    h('button', {
      class: [
        'inline-flex h-9 w-fit items-center justify-center rounded-md px-4',
        'bg-slate-900 text-sm font-medium text-white',
        'border border-transparent hover:bg-slate-800',
        'cursor-pointer',
        'focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
        'transition-none',
      ].join(' '),
      type: 'button',
      onClick: onAction,
    }, actionLabel),
  ])
}

// One segment per region: done, in progress (pulsing), pending.
function buildStepBar(total: number, completed: number): VNode {
  return h('div', { 'class': 'flex w-full gap-1', 'aria-hidden': 'true' }, Array.from({ length: total }, (_, index) => {
    const state = index < completed
      ? 'bg-emerald-500'
      : index === completed
        ? 'bg-amber-400 animate-pulse'
        : 'bg-slate-200'
    return h('span', { class: `h-1.5 flex-1 rounded-full ${state}` })
  }))
}

function getToastTitle() {
  return i18n.global.t('replication-toast-title')
}

// Shows only the region being replicated now, so the toast stays compact on phones.
function buildDescription(
  regions: DeploymentRegion[],
  completed: number,
  remainingMs: number,
  actionLabel?: string,
  onAction?: () => void,
): VNode {
  const total = regions.length
  const safeCompleted = Math.min(completed, total)
  const current = regions[Math.min(safeCompleted, total - 1)]
  const actionButton = buildActionButton(actionLabel, onAction)

  return h('div', { class: 'w-full flex flex-col gap-2' }, [
    h('div', { class: 'flex items-center gap-2 min-w-0' }, [
      h('span', { class: 'inline-block h-2 w-2 shrink-0 rounded-full bg-amber-400 animate-pulse' }),
      h('span', { class: 'min-w-0 flex-1 truncate text-sm font-medium text-slate-800' }, current?.label ?? ''),
      h('span', { class: 'shrink-0 text-xs tabular-nums text-slate-500' }, `${Math.min(safeCompleted + 1, total)}/${total}`),
    ]),
    buildStepBar(total, safeCompleted),
    h('div', { class: 'flex justify-between gap-2 text-xs text-slate-500' }, [
      h('span', i18n.global.t('replication-toast-regions-replicated', { completed: safeCompleted, total })),
      h('span', { class: 'tabular-nums' }, formatDuration(Math.max(0, Math.ceil(remainingMs / 1000)))),
    ]),
    ...(actionButton ? [actionButton] : []),
  ])
}

function buildDoneDescription(regions: DeploymentRegion[], actionLabel?: string, onAction?: () => void): VNode {
  const actionButton = buildActionButton(actionLabel, onAction)

  return h('div', { class: 'w-full flex flex-col gap-2' }, [
    buildStepBar(regions.length, regions.length),
    h('div', { class: 'text-xs text-slate-600' }, i18n.global.t('replication-toast-complete')),
    ...(actionButton ? [actionButton] : []),
  ])
}

function getCompletedRegionCount(elapsedMs: number, regionCount: number): number {
  if (regionCount <= 0)
    return 0
  const clampedElapsed = Math.min(Math.max(elapsedMs, 0), TOTAL_REPLICATION_MS)
  return Math.floor((clampedElapsed / TOTAL_REPLICATION_MS) * regionCount)
}

export function isUploadReplicationEvent(eventLabel: string): boolean {
  const event = eventLabel.toLowerCase()
  return event.includes('upload') && (event.includes('bundle') || event.includes('update') || event.includes('version'))
}

export function showUploadReplicationToast({
  eventLabel: _eventLabel,
  route,
  actionLabel,
  onAction,
}: DeploymentToastContext): void {
  const regions = getOrderedRegions()
  if (!regions.length)
    return

  const hasAction = route && onAction && actionLabel

  const regionCount = regions.length
  const startedAt = Date.now()
  let finished = false
  let intervalId: Timer | null = null
  let timeoutId: Timeout | null = null
  let toastId: string | number

  const update = () => {
    if (finished)
      return

    const elapsed = Date.now() - startedAt
    const remaining = Math.max(0, TOTAL_REPLICATION_MS - elapsed)
    const completed = getCompletedRegionCount(elapsed, regionCount)
    toast(getToastTitle(), {
      id: toastId,
      descriptionClass: 'w-full',
      classes: REPLICATION_TOAST_CLASSES,
      description: buildDescription(
        regions,
        completed,
        remaining,
        hasAction ? actionLabel : undefined,
        hasAction ? onAction : undefined,
      ),
      duration: remaining + 5000,
    })
  }

  const cleanup = () => {
    if (finished)
      return
    finished = true
    if (intervalId)
      clearInterval(intervalId)
    if (timeoutId)
      clearTimeout(timeoutId)
  }

  const finalize = () => {
    if (finished)
      return
    cleanup()
    toast(i18n.global.t('replication-toast-globally-available'), {
      id: toastId,
      descriptionClass: 'w-full',
      classes: REPLICATION_TOAST_CLASSES,
      style: REPLICATION_DONE_TOAST_STYLE,
      description: buildDoneDescription(
        regions,
        hasAction ? actionLabel : undefined,
        hasAction ? onAction : undefined,
      ),
      duration: 6000,
    })
  }

  const toastTitle = getToastTitle()
  toastId = toast(toastTitle, {
    descriptionClass: 'w-full',
    classes: REPLICATION_TOAST_CLASSES,
    description: buildDescription(
      regions,
      0,
      TOTAL_REPLICATION_MS,
      hasAction ? actionLabel : undefined,
      hasAction ? onAction : undefined,
    ),
    duration: TOTAL_REPLICATION_MS + 5000,
    onDismiss: cleanup,
  })

  intervalId = setInterval(update, UPDATE_INTERVAL_MS)
  timeoutId = setTimeout(() => finalize(), TOTAL_REPLICATION_MS)
}