Skip to content

Commit

Permalink
feat(synchronize): Init
Browse files Browse the repository at this point in the history
Signed-off-by: jld3103 <[email protected]>
  • Loading branch information
provokateurin committed Jan 31, 2024
1 parent 79d20d0 commit ca3fb72
Show file tree
Hide file tree
Showing 17 changed files with 1,113 additions and 0 deletions.
1 change: 1 addition & 0 deletions commitlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ rules:
- nextcloud_test
- release
- sort_box
- synchronize
- tool
1 change: 1 addition & 0 deletions packages/synchronize/LICENSE
3 changes: 3 additions & 0 deletions packages/synchronize/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# synchronize

A simple generic implementation of https://unterwaditzer.net/2016/sync-algorithm.html
1 change: 1 addition & 0 deletions packages/synchronize/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:neon_lints/dart.yaml
60 changes: 60 additions & 0 deletions packages/synchronize/lib/src/action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:meta/meta.dart';
import 'package:synchronize/src/object.dart';

/// Action to be executed in the sync process.
@internal
@immutable
sealed class SyncAction<T> {
/// Creates a new action.
const SyncAction(this.object);

/// The object that is part of the action.
final SyncObject<T> object;

@override
String toString() => 'SyncAction<$T>(object: $object)';
}

/// Action to delete on object from A.
@internal
@immutable
interface class SyncActionDeleteFromA<T1, T2> extends SyncAction<T1> {
/// Creates a new action to delete an object from A.
const SyncActionDeleteFromA(super.object);

@override
String toString() => 'SyncActionDeleteFromA<$T1, $T2>(object: $object)';
}

/// Action to delete an object from B.
@internal
@immutable
interface class SyncActionDeleteFromB<T1, T2> extends SyncAction<T2> {
/// Creates a new action to delete an object from B.
const SyncActionDeleteFromB(super.object);

@override
String toString() => 'SyncActionDeleteFromB<$T1, $T2>(object: $object)';
}

/// Action to write an object to A.
@internal
@immutable
interface class SyncActionWriteToA<T1, T2> extends SyncAction<T2> {
/// Creates a new action to write an object to A.
const SyncActionWriteToA(super.object);

@override
String toString() => 'SyncActionWriteToA<$T1, $T2>(object: $object)';
}

/// Action to write an object to B.
@internal
@immutable
interface class SyncActionWriteToB<T1, T2> extends SyncAction<T1> {
/// Creates a new action to write an object to B.
const SyncActionWriteToB(super.object);

@override
String toString() => 'SyncActionWriteToB<$T1, $T2>(object: $object)';
}
61 changes: 61 additions & 0 deletions packages/synchronize/lib/src/conflict.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'package:meta/meta.dart';
import 'package:synchronize/src/object.dart';

/// Contains information about a conflict that appeared during sync.
@immutable
class SyncConflict<T1, T2> {
/// Creates a new conflict.
const SyncConflict({
required this.id,
required this.type,
required this.objectA,
required this.objectB,
this.skipped = false,
});

/// Id of the objects involved in the conflict.
final String id;

/// Type of the conflict that appeared. See [SyncConflictType] for more info.
final SyncConflictType type;

/// Object A involved in the conflict.
final SyncObject<T1> objectA;

/// Object B involved in the conflict.
final SyncObject<T2> objectB;

/// Whether the conflict was skipped by the user, useful for ignoring it later on.
final bool skipped;

@override
bool operator ==(dynamic other) => other is SyncConflict && other.id == id;

@override
int get hashCode => id.hashCode;

@override
String toString() =>
'SyncConflict<$T1, $T2>(id: $id, type: $type, objectA: $objectA, objectB: $objectB, skipped: $skipped)';
}

/// Types of conflicts that can appear during sync.
enum SyncConflictType {
/// New objects with the same id exist on both sides.
bothNew,

/// Both objects with the same id have changed.
bothChanged,
}

/// Ways to resolve [SyncConflict]s.
enum SyncConflictSolution {
/// Overwrite the content of object A with the content of object B.
overwriteA,

/// Overwrite the content of object B with the content of object A.
overwriteB,

/// Skip the conflict and just do nothing.
skip,
}
33 changes: 33 additions & 0 deletions packages/synchronize/lib/src/journal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:synchronize/src/journal_entry.dart';

part 'journal.g.dart';

/// Contains the journal.
///
/// Used for detecting changes and new or deleted files.
@JsonSerializable()
class SyncJournal {
/// Creates a new journal.
// Note: This must not be const as otherwise the entries are not modifiable when a const set is used!
SyncJournal([Set<SyncJournalEntry>? entries]) : entries = entries ?? {};

/// Deserializes a journal from [json].
factory SyncJournal.fromJson(Map<String, dynamic> json) => _$SyncJournalFromJson(json);

/// Serializes a journal to JSON.
Map<String, dynamic> toJson() => _$SyncJournalToJson(this);

/// All entries contained in the journal.
final Set<SyncJournalEntry> entries;

/// Updates an [entry].
void updateEntry(SyncJournalEntry entry) {
entries
..remove(entry)
..add(entry);
}

@override
String toString() => 'SyncJournal(entries: $entries)';
}
15 changes: 15 additions & 0 deletions packages/synchronize/lib/src/journal.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 52 additions & 0 deletions packages/synchronize/lib/src/journal_entry.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:synchronize/src/journal.dart';

part 'journal_entry.g.dart';

/// Stores a single entry in the [SyncJournal].
///
/// It contains an [id] and ETags for each object, [etagA] and [etagB] respectively.
@immutable
@JsonSerializable()
class SyncJournalEntry {
/// Creates a new journal entry.
const SyncJournalEntry(
this.id,
this.etagA,
this.etagB,
);

/// Deserializes a journal entry from [json].
factory SyncJournalEntry.fromJson(Map<String, dynamic> json) => _$SyncJournalEntryFromJson(json);

/// Serializes a journal entry to JSON.
Map<String, dynamic> toJson() => _$SyncJournalEntryToJson(this);

/// Unique ID of the journal entry.
final String id;

/// ETag of the object A.
final String etagA;

/// ETag of the object B.
final String etagB;

@override
bool operator ==(Object other) => other is SyncJournalEntry && other.id == id;

@override
int get hashCode => id.hashCode;

@override
String toString() => 'SyncJournalEntry(id: $id, etagA: $etagA, etagB: $etagB)';
}

/// Extension to find a [SyncJournalEntry].
extension SyncJournalEntriesFind on Iterable<SyncJournalEntry> {
/// Finds the first [SyncJournalEntry] that has the [SyncJournalEntry.id] set to [id].
///
/// Returns `null` if no matching [SyncJournalEntry] was found.
SyncJournalEntry? tryFind(String id) => firstWhereOrNull((entry) => entry.id == id);
}
19 changes: 19 additions & 0 deletions packages/synchronize/lib/src/journal_entry.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions packages/synchronize/lib/src/object.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'package:collection/collection.dart';

/// Wraps the actual data contained on each side.
typedef SyncObject<T> = ({String id, T data});

/// Extension to find a [SyncObject].
extension SyncObjectsFind<T> on Iterable<SyncObject<T>> {
/// Finds the first [SyncObject] that has the `id` set to [id].
///
/// Returns `null` if no matching [SyncObject] was found.
SyncObject<T>? tryFind(String id) => firstWhereOrNull((object) => object.id == id);
}
39 changes: 39 additions & 0 deletions packages/synchronize/lib/src/sources.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:async';

import 'package:meta/meta.dart';
import 'package:synchronize/src/conflict.dart';
import 'package:synchronize/src/object.dart';

/// The source the sync uses to sync from and to.
@immutable
abstract interface class SyncSource<T1, T2> {
/// List all the objects.
FutureOr<List<SyncObject<T1>>> listObjects();

/// Calculates the ETag of a given [object].
///
/// Must be something easy to compute like the mtime of a file and preferably not the hash of the whole content in order to be fast.
FutureOr<String> getObjectETag(SyncObject<T1> object);

/// Writes the given [object].
FutureOr<SyncObject<T1>> writeObject(SyncObject<T2> object);

/// Deletes the given [object].
FutureOr<void> deleteObject(SyncObject<T1> object);
}

/// The sources the sync uses to sync from and to.
@immutable
abstract interface class SyncSources<T1, T2> {
/// Source A.
SyncSource<T1, T2> get sourceA;

/// Source B.
SyncSource<T2, T1> get sourceB;

/// Automatically find a solution for conflicts that don't matter. Useful e.g. for ignoring new directories.
SyncConflictSolution? findSolution(SyncObject<T1> objectA, SyncObject<T2> objectB);

@override
String toString() => 'SyncSources<$T1, $T2>(sourceA: $sourceA, sourceB: $sourceB)';
}
Loading

0 comments on commit ca3fb72

Please sign in to comment.