Server: - GET/PUT /api/v1/sync/presets — upsert with last-write-wins - DELETE /api/v1/sync/presets/:id - GET/PUT /api/v1/sync/progress — game progress upsert Client: - syncService.js: offline-first sync layer - localStorage remains primary store - Pushes to server when logged in - Merges server data into local on sync - Auto-sync every 30s + on tab focus - AuthContext starts/stops sync on login/logout - Sync runs on session restore (refresh token) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
135 lines
3.3 KiB
JavaScript
135 lines
3.3 KiB
JavaScript
import { getAccessToken } from './api.js';
|
|
|
|
const API_BASE = '/api/v1/sync';
|
|
const SYNC_INTERVAL = 30000; // 30 seconds
|
|
|
|
let _syncTimer = null;
|
|
|
|
async function apiFetch(method, path, body = null) {
|
|
const token = getAccessToken();
|
|
if (!token) return null;
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
credentials: 'include',
|
|
body: body ? JSON.stringify(body) : null,
|
|
});
|
|
|
|
if (!res.ok) return null;
|
|
return res.json();
|
|
}
|
|
|
|
// ==================== Preset Sync ====================
|
|
|
|
export async function syncPresets() {
|
|
if (!getAccessToken()) return;
|
|
|
|
try {
|
|
// Get local presets
|
|
const localRaw = localStorage.getItem('reaktor_presets');
|
|
const localPresets = localRaw ? JSON.parse(localRaw) : [];
|
|
|
|
// Get server presets
|
|
const serverData = await apiFetch('GET', '/presets');
|
|
if (!serverData) return;
|
|
|
|
// Push local presets to server
|
|
if (localPresets.length > 0) {
|
|
await apiFetch('PUT', '/presets', {
|
|
presets: localPresets.map(p => ({
|
|
name: p.name,
|
|
data: p.data,
|
|
updatedAt: p.savedAt || new Date().toISOString(),
|
|
})),
|
|
});
|
|
}
|
|
|
|
// Merge server presets into local (add any missing)
|
|
const serverPresets = serverData.presets || [];
|
|
const localNames = new Set(localPresets.map(p => p.name));
|
|
let merged = [...localPresets];
|
|
|
|
for (const sp of serverPresets) {
|
|
if (!localNames.has(sp.name)) {
|
|
merged.push({
|
|
name: sp.name,
|
|
data: sp.data,
|
|
savedAt: sp.updatedAt,
|
|
});
|
|
}
|
|
}
|
|
|
|
localStorage.setItem('reaktor_presets', JSON.stringify(merged));
|
|
} catch (err) {
|
|
console.warn('[sync] preset sync failed:', err.message);
|
|
}
|
|
}
|
|
|
|
// ==================== Game Progress Sync ====================
|
|
|
|
export async function syncProgress() {
|
|
if (!getAccessToken()) return;
|
|
|
|
try {
|
|
const localRaw = localStorage.getItem('synthquest-progress');
|
|
const localProgress = localRaw ? JSON.parse(localRaw) : null;
|
|
|
|
// Get server progress
|
|
const serverData = await apiFetch('GET', '/progress');
|
|
|
|
if (localProgress) {
|
|
// Push local to server
|
|
await apiFetch('PUT', '/progress', {
|
|
data: localProgress,
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
}
|
|
|
|
// If server has progress and local doesn't, pull it
|
|
if (serverData?.progress && !localProgress) {
|
|
localStorage.setItem('synthquest-progress', JSON.stringify(serverData.progress));
|
|
}
|
|
|
|
// Also sync hint data
|
|
const hintsRaw = localStorage.getItem('synthquest-hints');
|
|
// Hints are local-only for now (anti-cheat integrity)
|
|
} catch (err) {
|
|
console.warn('[sync] progress sync failed:', err.message);
|
|
}
|
|
}
|
|
|
|
// ==================== Auto Sync ====================
|
|
|
|
export function startAutoSync() {
|
|
if (_syncTimer) return;
|
|
|
|
// Initial sync
|
|
syncPresets();
|
|
syncProgress();
|
|
|
|
// Periodic sync
|
|
_syncTimer = setInterval(() => {
|
|
syncPresets();
|
|
syncProgress();
|
|
}, SYNC_INTERVAL);
|
|
|
|
// Sync on tab focus
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
syncPresets();
|
|
syncProgress();
|
|
}
|
|
});
|
|
}
|
|
|
|
export function stopAutoSync() {
|
|
if (_syncTimer) {
|
|
clearInterval(_syncTimer);
|
|
_syncTimer = null;
|
|
}
|
|
}
|