215 lines
6.4 KiB
TypeScript
215 lines
6.4 KiB
TypeScript
import apiService from './api.service'
|
||
import { store } from '@/store'
|
||
import { applicationConfigurationUrl } from './abpConfig.service'
|
||
import { MigrateLogEntry, MigrateLogLevel, SetupStatusDto } from '@/proxy/setup/models'
|
||
|
||
/**
|
||
* Sunucunun veritabanı/kurulum durumu.
|
||
*
|
||
* Hata durumu her çağıran tarafından ayrıca ele alınır (kurulum moduna geçiş kararı,
|
||
* durum kartı), bu yüzden global hata diyaloğu bastırılır.
|
||
*/
|
||
export const getSetupStatus = () =>
|
||
apiService.fetchData<SetupStatusDto>({
|
||
method: 'GET',
|
||
url: '/api/setup/application-status',
|
||
silentError: true,
|
||
})
|
||
|
||
const getMigrateUrl = (): string => {
|
||
const base = import.meta.env.VITE_API_URL ?? ''
|
||
return `${base}/api/setup/migrate`
|
||
}
|
||
|
||
/**
|
||
* Sunucu yeniden başlatıldıktan sonra tam ABP stack ile ayağa kalkana kadar
|
||
* application-configuration endpoint'ini poll eder.
|
||
*
|
||
* Arka arkaya iki başarılı yanıt beklenir; tek başarılı yanıt sunucu henüz
|
||
* tüm modülleri yüklemeden de dönebilir.
|
||
*
|
||
* @returns Poll'u iptal eden fonksiyon.
|
||
*/
|
||
export const pollUntilServerReady = (
|
||
onReady: () => void,
|
||
onAttempt?: (attempt: number) => void,
|
||
): (() => void) => {
|
||
const REQUIRED_CONSECUTIVE_SUCCESS = 2
|
||
const INITIAL_DELAY_MS = 3000
|
||
const SUCCESS_RETRY_MS = 1000
|
||
const FAILURE_RETRY_MS = 2000
|
||
|
||
let attempt = 0
|
||
let successCount = 0
|
||
let timerId: ReturnType<typeof setTimeout> | null = null
|
||
let cancelled = false
|
||
|
||
const tick = async () => {
|
||
if (cancelled) return
|
||
|
||
attempt++
|
||
onAttempt?.(attempt)
|
||
|
||
try {
|
||
const res = await fetch(
|
||
`${import.meta.env.VITE_API_URL ?? ''}${applicationConfigurationUrl(false)}`,
|
||
{ method: 'GET', headers: { Accept: 'application/json' }, cache: 'no-store' },
|
||
)
|
||
|
||
if (res.status === 200) {
|
||
const json = await res.json()
|
||
if (json && typeof json.currentUser === 'object') {
|
||
successCount++
|
||
if (successCount >= REQUIRED_CONSECUTIVE_SUCCESS) {
|
||
onReady()
|
||
return
|
||
}
|
||
timerId = setTimeout(tick, SUCCESS_RETRY_MS)
|
||
return
|
||
}
|
||
}
|
||
} catch {
|
||
/* sunucu henüz ayakta değil */
|
||
}
|
||
|
||
successCount = 0
|
||
timerId = setTimeout(tick, FAILURE_RETRY_MS)
|
||
}
|
||
|
||
// Sunucunun kapanması için kısa bir bekleme.
|
||
timerId = setTimeout(tick, INITIAL_DELAY_MS)
|
||
|
||
return () => {
|
||
cancelled = true
|
||
if (timerId) clearTimeout(timerId)
|
||
}
|
||
}
|
||
|
||
export interface MigrateStreamOptions {
|
||
/**
|
||
* Kurulum parolası. Veritabanı yokken (sunucu kurulum modundayken) zorunludur;
|
||
* canlı sistemde bunun yerine oturum token'ı ile yetkilendirme yapılır.
|
||
*/
|
||
setupPassword?: string
|
||
signal?: AbortSignal
|
||
/** Her log satırı için çağrılır. */
|
||
onLog?: (entry: MigrateLogEntry) => void
|
||
/** Migration başarıyla tamamlandığında. */
|
||
onSuccess?: () => void
|
||
/** Sunucu yeniden başlıyor — istemci hazır olana kadar poll etmeli. */
|
||
onRestart?: () => void
|
||
/** Akış hata ile bitti. */
|
||
onError?: (error: Error) => void
|
||
/** Başarılı veya başarısız — akış sona erdi. */
|
||
onFinished?: () => void
|
||
}
|
||
|
||
/**
|
||
* /api/setup/migrate SSE akışının tek istemcisi.
|
||
*
|
||
* Hem /setup sayfası hem SQL Query Manager hem de Wizard dosya yöneticisi bunu kullanır;
|
||
* SSE ayrıştırma mantığı yalnızca burada bulunur.
|
||
*/
|
||
export const streamSetupMigrate = async (options: MigrateStreamOptions = {}): Promise<void> => {
|
||
const { setupPassword, signal, onLog, onSuccess, onRestart, onError, onFinished } = options
|
||
|
||
// Kurulum parolası kullanılıyorsa oturum token'ı gönderilmez. Sunucu zaten parola
|
||
// yolunu seçtiğinde token'a bakmaz; token göndermek yalnızca veritabanına bağımlı
|
||
// ara katmanların (oturum doğrulama, dinamik claim'ler) veritabanı düşükken
|
||
// devreye girip isteği bloke etmesine yol açar.
|
||
const token = setupPassword ? null : store.getState().auth.session.token
|
||
let failed = false
|
||
|
||
const fail = (error: Error) => {
|
||
if (failed) return
|
||
failed = true
|
||
onError?.(error)
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(getMigrateUrl(), {
|
||
method: 'POST',
|
||
headers: {
|
||
Accept: 'text/event-stream',
|
||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||
...(setupPassword ? { 'X-Setup-Password': setupPassword } : {}),
|
||
},
|
||
signal,
|
||
})
|
||
|
||
if (!response.ok || !response.body) {
|
||
// Sunucu reddetme nedenini gövdede düz metin olarak döner (yanlış parola,
|
||
// eksik yapılandırma, eksik izin). Bunu yutmak hatayı teşhis edilemez yapıyordu.
|
||
const serverReason = await response.text().catch(() => '')
|
||
|
||
fail(
|
||
new Error(
|
||
serverReason.trim() ||
|
||
(response.status === 403
|
||
? 'Not authorized to run database migration.'
|
||
: `Server responded with status ${response.status}`),
|
||
),
|
||
)
|
||
return
|
||
}
|
||
|
||
const reader = response.body.getReader()
|
||
const decoder = new TextDecoder()
|
||
let buffer = ''
|
||
let sawSuccess = false
|
||
|
||
const handleEntry = (entry: MigrateLogEntry) => {
|
||
const level = (entry.level ?? 'info') as MigrateLogLevel
|
||
|
||
if (level === 'done') {
|
||
// Sunucu "done" gönderdi ama öncesinde success/restart gelmediyse akış başarısızdır.
|
||
if (!sawSuccess) fail(new Error(entry.message || 'Migration failed.'))
|
||
return
|
||
}
|
||
|
||
onLog?.(entry)
|
||
|
||
if (level === 'success') {
|
||
sawSuccess = true
|
||
onSuccess?.()
|
||
} else if (level === 'restart') {
|
||
sawSuccess = true
|
||
onRestart?.()
|
||
} else if (level === 'error') {
|
||
fail(new Error(entry.message || 'Migration failed.'))
|
||
}
|
||
}
|
||
|
||
const flushLines = (chunk: string) => {
|
||
buffer += chunk
|
||
const lines = buffer.split('\n')
|
||
buffer = lines.pop() ?? ''
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim()
|
||
if (!trimmed.startsWith('data:')) continue
|
||
|
||
const raw = trimmed.slice(5).trim()
|
||
if (!raw) continue
|
||
|
||
try {
|
||
handleEntry(JSON.parse(raw) as MigrateLogEntry)
|
||
} catch {
|
||
onLog?.({ level: 'info', message: raw })
|
||
}
|
||
}
|
||
}
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read()
|
||
if (done) break
|
||
flushLines(decoder.decode(value, { stream: true }))
|
||
}
|
||
} catch (err) {
|
||
if ((err as Error)?.name !== 'AbortError') {
|
||
fail(err instanceof Error ? err : new Error(String(err)))
|
||
}
|
||
} finally {
|
||
onFinished?.()
|
||
}
|
||
}
|