aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/sync/conflict.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/sync/conflict.ts')
-rw-r--r--src/client/sync/conflict.ts337
1 files changed, 333 insertions, 4 deletions
diff --git a/src/client/sync/conflict.ts b/src/client/sync/conflict.ts
index e4f1cbf..d9c3f55 100644
--- a/src/client/sync/conflict.ts
+++ b/src/client/sync/conflict.ts
@@ -1,6 +1,28 @@
-import type { LocalCard, LocalDeck } from "../db/index";
-import { localCardRepository, localDeckRepository } from "../db/repositories";
-import type { ServerCard, ServerDeck, SyncPullResult } from "./pull";
+import type {
+ LocalCard,
+ LocalDeck,
+ LocalNote,
+ LocalNoteFieldType,
+ LocalNoteFieldValue,
+ LocalNoteType,
+} from "../db/index";
+import {
+ localCardRepository,
+ localDeckRepository,
+ localNoteFieldTypeRepository,
+ localNoteFieldValueRepository,
+ localNoteRepository,
+ localNoteTypeRepository,
+} from "../db/repositories";
+import type {
+ ServerCard,
+ ServerDeck,
+ ServerNote,
+ ServerNoteFieldType,
+ ServerNoteFieldValue,
+ ServerNoteType,
+ SyncPullResult,
+} from "./pull";
import type { SyncPushResult } from "./push";
/**
@@ -17,6 +39,10 @@ export interface ConflictResolutionItem {
export interface ConflictResolutionResult {
decks: ConflictResolutionItem[];
cards: ConflictResolutionItem[];
+ noteTypes: ConflictResolutionItem[];
+ noteFieldTypes: ConflictResolutionItem[];
+ notes: ConflictResolutionItem[];
+ noteFieldValues: ConflictResolutionItem[];
}
/**
@@ -87,6 +113,79 @@ function serverCardToLocal(card: ServerCard): LocalCard {
}
/**
+ * Convert server note type to local format for storage
+ */
+function serverNoteTypeToLocal(noteType: ServerNoteType): LocalNoteType {
+ return {
+ id: noteType.id,
+ userId: noteType.userId,
+ name: noteType.name,
+ frontTemplate: noteType.frontTemplate,
+ backTemplate: noteType.backTemplate,
+ isReversible: noteType.isReversible,
+ createdAt: new Date(noteType.createdAt),
+ updatedAt: new Date(noteType.updatedAt),
+ deletedAt: noteType.deletedAt ? new Date(noteType.deletedAt) : null,
+ syncVersion: noteType.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server note field type to local format for storage
+ */
+function serverNoteFieldTypeToLocal(
+ fieldType: ServerNoteFieldType,
+): LocalNoteFieldType {
+ return {
+ id: fieldType.id,
+ noteTypeId: fieldType.noteTypeId,
+ name: fieldType.name,
+ order: fieldType.order,
+ fieldType: fieldType.fieldType as LocalNoteFieldType["fieldType"],
+ createdAt: new Date(fieldType.createdAt),
+ updatedAt: new Date(fieldType.updatedAt),
+ deletedAt: fieldType.deletedAt ? new Date(fieldType.deletedAt) : null,
+ syncVersion: fieldType.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server note to local format for storage
+ */
+function serverNoteToLocal(note: ServerNote): LocalNote {
+ return {
+ id: note.id,
+ deckId: note.deckId,
+ noteTypeId: note.noteTypeId,
+ createdAt: new Date(note.createdAt),
+ updatedAt: new Date(note.updatedAt),
+ deletedAt: note.deletedAt ? new Date(note.deletedAt) : null,
+ syncVersion: note.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
+ * Convert server note field value to local format for storage
+ */
+function serverNoteFieldValueToLocal(
+ fieldValue: ServerNoteFieldValue,
+): LocalNoteFieldValue {
+ return {
+ id: fieldValue.id,
+ noteId: fieldValue.noteId,
+ noteFieldTypeId: fieldValue.noteFieldTypeId,
+ value: fieldValue.value,
+ createdAt: new Date(fieldValue.createdAt),
+ updatedAt: new Date(fieldValue.updatedAt),
+ syncVersion: fieldValue.syncVersion,
+ _synced: true,
+ };
+}
+
+/**
* Conflict Resolver
*
* Handles conflicts reported by the server during push operations.
@@ -109,7 +208,11 @@ export class ConflictResolver {
hasConflicts(pushResult: SyncPushResult): boolean {
return (
pushResult.conflicts.decks.length > 0 ||
- pushResult.conflicts.cards.length > 0
+ pushResult.conflicts.cards.length > 0 ||
+ pushResult.conflicts.noteTypes.length > 0 ||
+ pushResult.conflicts.noteFieldTypes.length > 0 ||
+ pushResult.conflicts.notes.length > 0 ||
+ pushResult.conflicts.noteFieldValues.length > 0
);
}
@@ -200,6 +303,142 @@ export class ConflictResolver {
}
/**
+ * Resolve note type conflict using configured strategy
+ */
+ async resolveNoteTypeConflict(
+ localNoteType: LocalNoteType,
+ serverNoteType: ServerNoteType,
+ ): Promise<ConflictResolutionItem> {
+ let resolution: "server_wins" | "local_wins";
+
+ switch (this.strategy) {
+ case "server_wins":
+ resolution = "server_wins";
+ break;
+ case "local_wins":
+ resolution = "local_wins";
+ break;
+ case "newer_wins":
+ resolution = isServerNewer(
+ new Date(serverNoteType.updatedAt),
+ localNoteType.updatedAt,
+ )
+ ? "server_wins"
+ : "local_wins";
+ break;
+ }
+
+ if (resolution === "server_wins") {
+ const localData = serverNoteTypeToLocal(serverNoteType);
+ await localNoteTypeRepository.upsertFromServer(localData);
+ }
+
+ return { id: localNoteType.id, resolution };
+ }
+
+ /**
+ * Resolve note field type conflict using configured strategy
+ */
+ async resolveNoteFieldTypeConflict(
+ localFieldType: LocalNoteFieldType,
+ serverFieldType: ServerNoteFieldType,
+ ): Promise<ConflictResolutionItem> {
+ let resolution: "server_wins" | "local_wins";
+
+ switch (this.strategy) {
+ case "server_wins":
+ resolution = "server_wins";
+ break;
+ case "local_wins":
+ resolution = "local_wins";
+ break;
+ case "newer_wins":
+ resolution = isServerNewer(
+ new Date(serverFieldType.updatedAt),
+ localFieldType.updatedAt,
+ )
+ ? "server_wins"
+ : "local_wins";
+ break;
+ }
+
+ if (resolution === "server_wins") {
+ const localData = serverNoteFieldTypeToLocal(serverFieldType);
+ await localNoteFieldTypeRepository.upsertFromServer(localData);
+ }
+
+ return { id: localFieldType.id, resolution };
+ }
+
+ /**
+ * Resolve note conflict using configured strategy
+ */
+ async resolveNoteConflict(
+ localNote: LocalNote,
+ serverNote: ServerNote,
+ ): Promise<ConflictResolutionItem> {
+ let resolution: "server_wins" | "local_wins";
+
+ switch (this.strategy) {
+ case "server_wins":
+ resolution = "server_wins";
+ break;
+ case "local_wins":
+ resolution = "local_wins";
+ break;
+ case "newer_wins":
+ resolution = isServerNewer(
+ new Date(serverNote.updatedAt),
+ localNote.updatedAt,
+ )
+ ? "server_wins"
+ : "local_wins";
+ break;
+ }
+
+ if (resolution === "server_wins") {
+ const localData = serverNoteToLocal(serverNote);
+ await localNoteRepository.upsertFromServer(localData);
+ }
+
+ return { id: localNote.id, resolution };
+ }
+
+ /**
+ * Resolve note field value conflict using configured strategy
+ */
+ async resolveNoteFieldValueConflict(
+ localFieldValue: LocalNoteFieldValue,
+ serverFieldValue: ServerNoteFieldValue,
+ ): Promise<ConflictResolutionItem> {
+ let resolution: "server_wins" | "local_wins";
+
+ switch (this.strategy) {
+ case "server_wins":
+ resolution = "server_wins";
+ break;
+ case "local_wins":
+ resolution = "local_wins";
+ break;
+ case "newer_wins":
+ resolution = isServerNewer(
+ new Date(serverFieldValue.updatedAt),
+ localFieldValue.updatedAt,
+ )
+ ? "server_wins"
+ : "local_wins";
+ break;
+ }
+
+ if (resolution === "server_wins") {
+ const localData = serverNoteFieldValueToLocal(serverFieldValue);
+ await localNoteFieldValueRepository.upsertFromServer(localData);
+ }
+
+ return { id: localFieldValue.id, resolution };
+ }
+
+ /**
* Resolve all conflicts from a push result
* Uses pull result to get server data for conflicting items
*/
@@ -210,6 +449,10 @@ export class ConflictResolver {
const result: ConflictResolutionResult = {
decks: [],
cards: [],
+ noteTypes: [],
+ noteFieldTypes: [],
+ notes: [],
+ noteFieldValues: [],
};
// Resolve deck conflicts
@@ -252,6 +495,92 @@ export class ConflictResolver {
// If server doesn't have it but local does, keep local (will push again)
}
+ // Resolve note type conflicts
+ for (const noteTypeId of pushResult.conflicts.noteTypes) {
+ const localNoteType = await localNoteTypeRepository.findById(noteTypeId);
+ const serverNoteType = pullResult.noteTypes.find(
+ (nt) => nt.id === noteTypeId,
+ );
+
+ if (localNoteType && serverNoteType) {
+ const resolution = await this.resolveNoteTypeConflict(
+ localNoteType,
+ serverNoteType,
+ );
+ result.noteTypes.push(resolution);
+ } else if (serverNoteType) {
+ const localData = serverNoteTypeToLocal(serverNoteType);
+ await localNoteTypeRepository.upsertFromServer(localData);
+ result.noteTypes.push({ id: noteTypeId, resolution: "server_wins" });
+ }
+ }
+
+ // Resolve note field type conflicts
+ for (const fieldTypeId of pushResult.conflicts.noteFieldTypes) {
+ const localFieldType =
+ await localNoteFieldTypeRepository.findById(fieldTypeId);
+ const serverFieldType = pullResult.noteFieldTypes.find(
+ (ft) => ft.id === fieldTypeId,
+ );
+
+ if (localFieldType && serverFieldType) {
+ const resolution = await this.resolveNoteFieldTypeConflict(
+ localFieldType,
+ serverFieldType,
+ );
+ result.noteFieldTypes.push(resolution);
+ } else if (serverFieldType) {
+ const localData = serverNoteFieldTypeToLocal(serverFieldType);
+ await localNoteFieldTypeRepository.upsertFromServer(localData);
+ result.noteFieldTypes.push({
+ id: fieldTypeId,
+ resolution: "server_wins",
+ });
+ }
+ }
+
+ // Resolve note conflicts
+ for (const noteId of pushResult.conflicts.notes) {
+ const localNote = await localNoteRepository.findById(noteId);
+ const serverNote = pullResult.notes.find((n) => n.id === noteId);
+
+ if (localNote && serverNote) {
+ const resolution = await this.resolveNoteConflict(
+ localNote,
+ serverNote,
+ );
+ result.notes.push(resolution);
+ } else if (serverNote) {
+ const localData = serverNoteToLocal(serverNote);
+ await localNoteRepository.upsertFromServer(localData);
+ result.notes.push({ id: noteId, resolution: "server_wins" });
+ }
+ }
+
+ // Resolve note field value conflicts
+ for (const fieldValueId of pushResult.conflicts.noteFieldValues) {
+ const localFieldValue =
+ await localNoteFieldValueRepository.findById(fieldValueId);
+ const serverFieldValue = pullResult.noteFieldValues.find(
+ (fv) => fv.id === fieldValueId,
+ );
+
+ if (localFieldValue && serverFieldValue) {
+ const resolution = await this.resolveNoteFieldValueConflict(
+ localFieldValue,
+ serverFieldValue,
+ );
+ result.noteFieldValues.push(resolution);
+ } else if (serverFieldValue) {
+ const localData = serverNoteFieldValueToLocal(serverFieldValue);
+ await localNoteFieldValueRepository.upsertFromServer(localData);
+ result.noteFieldValues.push({
+ id: fieldValueId,
+ resolution: "server_wins",
+ });
+ }
+ }
+
return result;
}
}