broken: halfway through removing turso
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getDatabase } from '../database';
|
||||
import { syncLog, posts, media, NewSyncLogEntry } from '../database/schema';
|
||||
import { taskManager, Task } from './TaskManager';
|
||||
import { getPostEngine } from './PostEngine';
|
||||
import { getMediaEngine } from './MediaEngine';
|
||||
import { getDropboxSyncEngine } from './DropboxSyncEngine';
|
||||
|
||||
export type SyncDirection = 'push' | 'pull' | 'bidirectional';
|
||||
export type SyncStatus = 'idle' | 'syncing' | 'error';
|
||||
|
||||
export interface SyncConfig {
|
||||
tursoUrl: string;
|
||||
tursoAuthToken: string;
|
||||
autoSync: boolean;
|
||||
syncInterval: number; // in minutes
|
||||
}
|
||||
@@ -33,23 +29,37 @@ export interface SyncResult {
|
||||
};
|
||||
}
|
||||
|
||||
// Default timeout for sync operations (30 seconds)
|
||||
const DEFAULT_SYNC_TIMEOUT = 30000;
|
||||
|
||||
export class SyncEngine extends EventEmitter {
|
||||
private syncStatus: SyncStatus = 'idle';
|
||||
private syncConfig: SyncConfig | null = null;
|
||||
private syncIntervalId: NodeJS.Timeout | null = null;
|
||||
private syncTimeout: number = DEFAULT_SYNC_TIMEOUT;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getSyncTimeout(): number {
|
||||
return this.syncTimeout;
|
||||
}
|
||||
|
||||
setSyncTimeout(timeoutMs: number): void {
|
||||
this.syncTimeout = timeoutMs;
|
||||
}
|
||||
|
||||
getSyncStatus(): SyncStatus {
|
||||
return this.syncStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sync is configured. Uses Dropbox configuration status.
|
||||
*/
|
||||
isConfigured(): boolean {
|
||||
return this.syncConfig !== null &&
|
||||
!!this.syncConfig.tursoUrl &&
|
||||
!!this.syncConfig.tursoAuthToken;
|
||||
const dropboxEngine = getDropboxSyncEngine();
|
||||
return dropboxEngine.isConfigured();
|
||||
}
|
||||
|
||||
async configure(config: SyncConfig): Promise<void> {
|
||||
@@ -69,179 +79,9 @@ export class SyncEngine extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize remote database connection with the provided credentials
|
||||
const db = getDatabase();
|
||||
await db.initializeRemote({
|
||||
tursoUrl: config.tursoUrl,
|
||||
tursoAuthToken: config.tursoAuthToken,
|
||||
});
|
||||
|
||||
this.emit('configured', config);
|
||||
}
|
||||
|
||||
async sync(direction: SyncDirection = 'bidirectional'): Promise<SyncResult> {
|
||||
if (!this.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: ['Sync not configured'],
|
||||
};
|
||||
}
|
||||
|
||||
if (this.syncStatus === 'syncing') {
|
||||
return {
|
||||
success: false,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: ['Sync already in progress'],
|
||||
};
|
||||
}
|
||||
|
||||
const task: Task<SyncResult> = {
|
||||
id: uuidv4(),
|
||||
name: `Sync (${direction})`,
|
||||
execute: async (onProgress) => {
|
||||
this.syncStatus = 'syncing';
|
||||
this.emit('syncStarted', direction);
|
||||
|
||||
const result: SyncResult = {
|
||||
success: true,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const localDb = db.getLocal();
|
||||
const remoteDb = db.getRemote();
|
||||
|
||||
if (!remoteDb) {
|
||||
throw new Error('Remote database not initialized');
|
||||
}
|
||||
|
||||
onProgress(10, 'Fetching pending changes...');
|
||||
|
||||
if (direction === 'push' || direction === 'bidirectional') {
|
||||
// Get pending posts
|
||||
const pendingPosts = await localDb
|
||||
.select()
|
||||
.from(posts)
|
||||
.where(eq(posts.syncStatus, 'pending'))
|
||||
.all();
|
||||
|
||||
onProgress(20, `Pushing ${pendingPosts.length} posts...`);
|
||||
|
||||
for (const post of pendingPosts) {
|
||||
try {
|
||||
// Push to remote (simplified - in production would handle conflicts)
|
||||
await remoteDb.insert(posts).values(post).onConflictDoUpdate({
|
||||
target: posts.id,
|
||||
set: {
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
excerpt: post.excerpt,
|
||||
status: post.status,
|
||||
author: post.author,
|
||||
updatedAt: post.updatedAt,
|
||||
publishedAt: post.publishedAt,
|
||||
checksum: post.checksum,
|
||||
tags: post.tags,
|
||||
categories: post.categories,
|
||||
},
|
||||
});
|
||||
|
||||
// Mark as synced locally
|
||||
await localDb
|
||||
.update(posts)
|
||||
.set({ syncStatus: 'synced', syncedAt: new Date() })
|
||||
.where(eq(posts.id, post.id));
|
||||
|
||||
result.pushed++;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
result.errors.push(`Failed to push post ${post.id}: ${errorMsg}`);
|
||||
|
||||
// Log the error
|
||||
await this.logSyncOperation(post.id, 'post', 'update', 'failed', errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending media
|
||||
const pendingMedia = await localDb
|
||||
.select()
|
||||
.from(media)
|
||||
.where(eq(media.syncStatus, 'pending'))
|
||||
.all();
|
||||
|
||||
onProgress(50, `Pushing ${pendingMedia.length} media items...`);
|
||||
|
||||
for (const item of pendingMedia) {
|
||||
try {
|
||||
await remoteDb.insert(media).values(item).onConflictDoUpdate({
|
||||
target: media.id,
|
||||
set: {
|
||||
alt: item.alt,
|
||||
caption: item.caption,
|
||||
updatedAt: item.updatedAt,
|
||||
checksum: item.checksum,
|
||||
tags: item.tags,
|
||||
},
|
||||
});
|
||||
|
||||
await localDb
|
||||
.update(media)
|
||||
.set({ syncStatus: 'synced', syncedAt: new Date() })
|
||||
.where(eq(media.id, item.id));
|
||||
|
||||
result.pushed++;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
result.errors.push(`Failed to push media ${item.id}: ${errorMsg}`);
|
||||
|
||||
await this.logSyncOperation(item.id, 'media', 'update', 'failed', errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (direction === 'pull' || direction === 'bidirectional') {
|
||||
onProgress(70, 'Pulling remote changes...');
|
||||
|
||||
// In a real implementation, we would:
|
||||
// 1. Fetch all remote records with syncedAt > local last sync
|
||||
// 2. Compare checksums to detect conflicts
|
||||
// 3. Apply or merge changes
|
||||
|
||||
// For now, this is a placeholder
|
||||
onProgress(90, 'Pull complete');
|
||||
}
|
||||
|
||||
onProgress(100, 'Sync complete');
|
||||
|
||||
this.syncStatus = 'idle';
|
||||
this.emit('syncCompleted', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
result.success = false;
|
||||
result.errors.push(errorMsg);
|
||||
|
||||
this.syncStatus = 'error';
|
||||
this.emit('syncFailed', errorMsg);
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return taskManager.runTask(task);
|
||||
}
|
||||
|
||||
private async logSyncOperation(
|
||||
entityId: string,
|
||||
entityType: 'post' | 'media',
|
||||
@@ -324,38 +164,110 @@ export class SyncEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Full sync: metadata via Turso + files via Dropbox.
|
||||
* Coordinates both sync engines for a complete bidirectional sync.
|
||||
* Full sync: Files via Dropbox.
|
||||
* Synchronizes posts and media files to Dropbox for backup and cross-device access.
|
||||
*/
|
||||
async fullSync(direction: SyncDirection = 'bidirectional'): Promise<SyncResult> {
|
||||
// Run metadata sync (Turso)
|
||||
const metadataResult = await this.sync(direction);
|
||||
|
||||
// Run file sync (Dropbox) if configured
|
||||
const dropboxEngine = getDropboxSyncEngine();
|
||||
if (dropboxEngine.isConfigured()) {
|
||||
try {
|
||||
const fileResult = await dropboxEngine.syncAll();
|
||||
metadataResult.dropboxResult = {
|
||||
uploaded: fileResult.uploaded,
|
||||
downloaded: fileResult.downloaded,
|
||||
deleted: fileResult.deleted,
|
||||
conflicts: fileResult.conflicts,
|
||||
errors: fileResult.errors,
|
||||
};
|
||||
|
||||
if (!fileResult.success) {
|
||||
metadataResult.errors.push(
|
||||
...fileResult.errors.map(e => `Dropbox: ${e}`)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : 'Unknown Dropbox error';
|
||||
metadataResult.errors.push(`Dropbox sync failed: ${msg}`);
|
||||
}
|
||||
if (this.syncStatus === 'syncing') {
|
||||
return {
|
||||
success: false,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: ['Sync already in progress'],
|
||||
};
|
||||
}
|
||||
|
||||
return metadataResult;
|
||||
const result: SyncResult = {
|
||||
success: true,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const dropboxEngine = getDropboxSyncEngine();
|
||||
if (!dropboxEngine.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: ['Dropbox sync not configured'],
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[SyncEngine] Starting Dropbox file sync...');
|
||||
|
||||
const task: Task<SyncResult> = {
|
||||
id: uuidv4(),
|
||||
name: `Sync (${direction})`,
|
||||
execute: async (onProgress) => {
|
||||
this.syncStatus = 'syncing';
|
||||
this.emit('syncStarted', direction);
|
||||
|
||||
try {
|
||||
onProgress(10, 'Starting Dropbox sync...');
|
||||
|
||||
const fileResult = await dropboxEngine.syncAll();
|
||||
|
||||
result.dropboxResult = {
|
||||
uploaded: fileResult.uploaded,
|
||||
downloaded: fileResult.downloaded,
|
||||
deleted: fileResult.deleted,
|
||||
conflicts: fileResult.conflicts,
|
||||
errors: fileResult.errors,
|
||||
};
|
||||
|
||||
result.pushed = fileResult.uploaded;
|
||||
result.pulled = fileResult.downloaded;
|
||||
result.conflicts = fileResult.conflicts;
|
||||
|
||||
if (!fileResult.success) {
|
||||
result.success = false;
|
||||
result.errors.push(...fileResult.errors.map(e => `Dropbox: ${e}`));
|
||||
}
|
||||
|
||||
onProgress(100, 'Sync complete');
|
||||
console.log('[SyncEngine] Dropbox sync complete:', {
|
||||
uploaded: fileResult.uploaded,
|
||||
downloaded: fileResult.downloaded,
|
||||
errors: fileResult.errors.length,
|
||||
});
|
||||
|
||||
this.syncStatus = 'idle';
|
||||
this.emit('syncCompleted', result);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[SyncEngine] Sync failed:', errorMsg);
|
||||
result.success = false;
|
||||
result.errors.push(`Dropbox sync failed: ${errorMsg}`);
|
||||
|
||||
this.syncStatus = 'error';
|
||||
this.emit('syncFailed', errorMsg);
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
return await taskManager.runTask(task);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('[SyncEngine] Task manager error:', errorMsg);
|
||||
this.syncStatus = 'error';
|
||||
this.emit('syncFailed', errorMsg);
|
||||
return {
|
||||
success: false,
|
||||
pushed: 0,
|
||||
pulled: 0,
|
||||
conflicts: 0,
|
||||
errors: [errorMsg],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user