src/services/updateReplicationToast.ts
Rama main · réplica de lectura del repositorio original.
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)
}