cache versions
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -75,6 +75,9 @@ build/
|
|||||||
out/
|
out/
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# Version file (generated at build time)
|
||||||
|
public/version.json
|
||||||
|
|
||||||
# Backup files
|
# Backup files
|
||||||
*.bak
|
*.bak
|
||||||
*.backup
|
*.backup
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
"description": "Telegram miniapp for YouTube music search and MP3 conversion",
|
"description": "Telegram miniapp for YouTube music search and MP3 conversion",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && tsc -p tsconfig.frontend.json",
|
"build": "node scripts/generate-version.js && tsc && tsc -p tsconfig.frontend.json",
|
||||||
"build:backend": "tsc",
|
"build:backend": "tsc",
|
||||||
"build:frontend": "tsc -p tsconfig.frontend.json",
|
"build:frontend": "tsc -p tsconfig.frontend.json",
|
||||||
"build:prod": "yarn build && node scripts/minify.js",
|
"build:prod": "node scripts/generate-version.js && yarn build && node scripts/minify.js",
|
||||||
"minify": "node scripts/minify.js",
|
"minify": "node scripts/minify.js",
|
||||||
|
"version": "node scripts/generate-version.js",
|
||||||
"start": "node dist/server.js",
|
"start": "node dist/server.js",
|
||||||
"dev": "ts-node src/server.ts",
|
"dev": "ts-node src/server.ts",
|
||||||
"dev:watch": "nodemon --exec ts-node src/server.ts",
|
"dev:watch": "nodemon --exec ts-node src/server.ts",
|
||||||
|
|||||||
@@ -12,6 +12,11 @@
|
|||||||
<!-- Canonical URL -->
|
<!-- Canonical URL -->
|
||||||
<link rel="canonical" href="https://music.quixy.uk/">
|
<link rel="canonical" href="https://music.quixy.uk/">
|
||||||
|
|
||||||
|
<!-- Cache Control for iOS -->
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
|
||||||
<!-- Theme & App -->
|
<!-- Theme & App -->
|
||||||
<meta name="theme-color" content="#007AFF">
|
<meta name="theme-color" content="#007AFF">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|||||||
103
public/script.ts
103
public/script.ts
@@ -62,12 +62,115 @@ class QuixoticApp {
|
|||||||
private hasMoreResults: boolean = false;
|
private hasMoreResults: boolean = false;
|
||||||
private isLoadingMore: boolean = false;
|
private isLoadingMore: boolean = false;
|
||||||
private scrollObserver: IntersectionObserver | null = null;
|
private scrollObserver: IntersectionObserver | null = null;
|
||||||
|
private currentVersion: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tg = (window as WindowWithTelegram).Telegram?.WebApp;
|
this.tg = (window as WindowWithTelegram).Telegram?.WebApp;
|
||||||
this.loadRecentSearches();
|
this.loadRecentSearches();
|
||||||
this.init();
|
this.init();
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
|
this.checkVersion(); // Check for updates on load
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkVersion(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get current version from localStorage
|
||||||
|
const storedVersion = localStorage.getItem('appVersion');
|
||||||
|
|
||||||
|
// Fetch latest version from server
|
||||||
|
const response = await fetch('/api/version', {
|
||||||
|
cache: 'no-cache',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const versionData = await response.json();
|
||||||
|
const serverVersion = versionData.version;
|
||||||
|
|
||||||
|
this.currentVersion = serverVersion;
|
||||||
|
|
||||||
|
// If versions don't match, force reload
|
||||||
|
if (storedVersion && storedVersion !== serverVersion) {
|
||||||
|
console.log('🔄 New version detected, updating...');
|
||||||
|
|
||||||
|
// Clear cache and reload
|
||||||
|
if ('caches' in window) {
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
await Promise.all(cacheNames.map(name => caches.delete(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store new version
|
||||||
|
localStorage.setItem('appVersion', serverVersion);
|
||||||
|
|
||||||
|
// Force hard reload
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store version for future checks
|
||||||
|
if (!storedVersion) {
|
||||||
|
localStorage.setItem('appVersion', serverVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Periodically check for updates (every 5 minutes)
|
||||||
|
setInterval(() => this.silentVersionCheck(), 5 * 60 * 1000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Version check failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async silentVersionCheck(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/version', {
|
||||||
|
cache: 'no-cache',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) return;
|
||||||
|
|
||||||
|
const versionData = await response.json();
|
||||||
|
const serverVersion = versionData.version;
|
||||||
|
|
||||||
|
if (this.currentVersion && this.currentVersion !== serverVersion) {
|
||||||
|
console.log('🔄 Update available');
|
||||||
|
|
||||||
|
// Show update notification
|
||||||
|
this.showUpdateNotification();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Silent version check failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showUpdateNotification(): void {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = 'tg-update-notification';
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="tg-update-notification__content">
|
||||||
|
<span>Доступно обновление</span>
|
||||||
|
<button class="tg-update-notification__button">Обновить</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const button = notification.querySelector('button');
|
||||||
|
button?.addEventListener('click', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto-dismiss after 30 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private triggerHaptic(type: 'light' | 'medium' | 'heavy' | 'success' | 'error' = 'light'): void {
|
private triggerHaptic(type: 'light' | 'medium' | 'heavy' | 'success' | 'error' = 'light'): void {
|
||||||
|
|||||||
@@ -696,6 +696,45 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Update notification */
|
||||||
|
.tg-update-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: var(--tg-spacing-lg);
|
||||||
|
left: var(--tg-spacing-lg);
|
||||||
|
right: var(--tg-spacing-lg);
|
||||||
|
z-index: 1000;
|
||||||
|
animation: tg-slide-down 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-update-notification__content {
|
||||||
|
background: var(--tg-color-button);
|
||||||
|
color: var(--tg-color-button-text);
|
||||||
|
padding: var(--tg-spacing-md) var(--tg-spacing-lg);
|
||||||
|
border-radius: var(--tg-border-radius);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
font-weight: var(--tg-font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-update-notification__button {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: var(--tg-color-button-text);
|
||||||
|
border: none;
|
||||||
|
padding: var(--tg-spacing-sm) var(--tg-spacing-lg);
|
||||||
|
border-radius: var(--tg-border-radius-small);
|
||||||
|
font-size: var(--tg-font-size-sm);
|
||||||
|
font-weight: var(--tg-font-weight-semibold);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tg-update-notification__button:active {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* Recent Searches */
|
/* Recent Searches */
|
||||||
.tg-recent-searches {
|
.tg-recent-searches {
|
||||||
margin-bottom: var(--tg-spacing-lg);
|
margin-bottom: var(--tg-spacing-lg);
|
||||||
|
|||||||
46
scripts/generate-version.js
Normal file
46
scripts/generate-version.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
function generateVersion() {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
|
||||||
|
let gitHash = null;
|
||||||
|
let gitBranch = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Warning: Could not get git info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = {
|
||||||
|
timestamp,
|
||||||
|
date,
|
||||||
|
version: gitHash ? `${timestamp}-${gitHash}` : timestamp.toString(),
|
||||||
|
gitHash,
|
||||||
|
gitBranch,
|
||||||
|
buildDate: date
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputPath = path.join(__dirname, '../public/version.json');
|
||||||
|
fs.writeFileSync(outputPath, JSON.stringify(version, null, 2));
|
||||||
|
|
||||||
|
console.log('✅ Version file generated:', version.version);
|
||||||
|
console.log(` Date: ${date}`);
|
||||||
|
if (gitHash) {
|
||||||
|
console.log(` Git: ${gitHash} (${gitBranch})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
generateVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateVersion };
|
||||||
@@ -69,6 +69,19 @@ if (!fs.existsSync(downloadsDir)) {
|
|||||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load version for cache busting
|
||||||
|
let appVersion = Date.now().toString();
|
||||||
|
try {
|
||||||
|
const versionPath = path.join(__dirname, '../public/version.json');
|
||||||
|
if (fs.existsSync(versionPath)) {
|
||||||
|
const versionData = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
|
||||||
|
appVersion = versionData.version || appVersion;
|
||||||
|
logger.info(`App version loaded: ${appVersion}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Could not load version file, using timestamp');
|
||||||
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.get('/', (req: Request, res: Response) => {
|
app.get('/', (req: Request, res: Response) => {
|
||||||
// Use minified HTML in production
|
// Use minified HTML in production
|
||||||
@@ -76,12 +89,23 @@ app.get('/', (req: Request, res: Response) => {
|
|||||||
const htmlFile = isProduction ? 'index.min.html' : 'index.html';
|
const htmlFile = isProduction ? 'index.min.html' : 'index.html';
|
||||||
const indexPath = path.join(__dirname, '../public', htmlFile);
|
const indexPath = path.join(__dirname, '../public', htmlFile);
|
||||||
|
|
||||||
// Set cache headers for HTML (short cache)
|
// Set cache headers for HTML (no cache for HTML itself)
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control': 'public, max-age=3600, must-revalidate', // 1 hour, revalidate
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Read HTML and inject version
|
||||||
|
try {
|
||||||
|
let html = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
// Replace version placeholders with actual version
|
||||||
|
html = html.replace(/\?v=\d+/g, `?v=${appVersion}`);
|
||||||
|
res.send(html);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error serving HTML:', error);
|
||||||
res.sendFile(indexPath);
|
res.sendFile(indexPath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search videos
|
// Search videos
|
||||||
@@ -288,6 +312,22 @@ app.get('/health', (req: Request, res: Response) => {
|
|||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Version endpoint for client-side cache busting
|
||||||
|
app.get('/api/version', (req: Request, res: Response) => {
|
||||||
|
res.set('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
try {
|
||||||
|
const versionPath = path.join(__dirname, '../public/version.json');
|
||||||
|
if (fs.existsSync(versionPath)) {
|
||||||
|
const versionData = fs.readFileSync(versionPath, 'utf8');
|
||||||
|
res.json(JSON.parse(versionData));
|
||||||
|
} else {
|
||||||
|
res.json({ version: appVersion, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ version: appVersion, timestamp: Date.now() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use((err: Error, _req: Request, res: Response, _next: any) => {
|
app.use((err: Error, _req: Request, res: Response, _next: any) => {
|
||||||
logger.error(err.stack || err.message);
|
logger.error(err.stack || err.message);
|
||||||
|
|||||||
Reference in New Issue
Block a user