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 Aug 30, 2023
1 parent b552f07 commit 218082a
Show file tree
Hide file tree
Showing 20 changed files with 1,108 additions and 1 deletion.
1 change: 1 addition & 0 deletions commitlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ rules:
- neon_lints
- nextcloud
- sort_box
- synchronize
30 changes: 30 additions & 0 deletions packages/synchronize/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
10 changes: 10 additions & 0 deletions packages/synchronize/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: 2aa348b9407e96ffe4eca8e8f213c7984afad3f7
channel: master

project_type: package
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
40 changes: 40 additions & 0 deletions packages/synchronize/lib/src/action.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:meta/meta.dart';
import 'package:synchronize/src/object.dart';

/// Action to be executed in the sync process
@internal
sealed class SyncAction<T> {
// ignore: public_member_api_docs
SyncAction(this.object);

// ignore: public_member_api_docs
final SyncObject<T> object;
}

/// Action to delete object from A
@internal
interface class SyncActionDeleteFromA<T1, T2> extends SyncAction<T1> {
// ignore: public_member_api_docs
SyncActionDeleteFromA(super.object);
}

/// Action to delete object from B
@internal
interface class SyncActionDeleteFromB<T1, T2> extends SyncAction<T2> {
// ignore: public_member_api_docs
SyncActionDeleteFromB(super.object);
}

/// Action to write object to A
@internal
interface class SyncActionWriteToA<T1, T2> extends SyncAction<T2> {
// ignore: public_member_api_docs
SyncActionWriteToA(super.object);
}

/// Action to write object to B
@internal
interface class SyncActionWriteToB<T1, T2> extends SyncAction<T1> {
// ignore: public_member_api_docs
SyncActionWriteToB(super.object);
}
57 changes: 57 additions & 0 deletions packages/synchronize/lib/src/conflict.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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> {
// ignore: public_member_api_docs
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 ==(final dynamic other) => other is SyncConflict && other.id == id;

@override
int get hashCode => id.hashCode;
}

/// 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,
}
31 changes: 31 additions & 0 deletions packages/synchronize/lib/src/journal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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 {
// Note: This must not be const as otherwise the entries are not modifiable when a const set is used!
// ignore: public_member_api_docs
SyncJournal(
this.entries,
);

// ignore: public_member_api_docs
factory SyncJournal.fromJson(final Map<String, dynamic> json) => _$SyncJournalFromJson(json);
// ignore: public_member_api_docs
Map<String, dynamic> toJson() => _$SyncJournalToJson(this);

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

/// Update an [entry].
void updateEntry(final SyncJournalEntry entry) {
entries
..remove(entry)
..add(entry);
}
}
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.

49 changes: 49 additions & 0 deletions packages/synchronize/lib/src/journal_entry.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 {
// ignore: public_member_api_docs
const SyncJournalEntry(
this.id,
this.etagA,
this.etagB,
);

// ignore: public_member_api_docs
factory SyncJournalEntry.fromJson(final Map<String, dynamic> json) => _$SyncJournalEntryFromJson(json);
// ignore: public_member_api_docs
Map<String, dynamic> toJson() => _$SyncJournalEntryToJson(this);

// ignore: public_member_api_docs
final String id;

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

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

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

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

@override
int get hashCode => id.hashCode;
}

// ignore: public_member_api_docs
extension SyncJournalEntriesFind on Iterable<SyncJournalEntry> {
// ignore: public_member_api_docs
SyncJournalEntry? tryFind(final String id) => firstWhereOrNull((final 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.

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

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

// ignore: public_member_api_docs
extension SyncObjectFields<T> on SyncObject<T> {
/// Id of the object.
String get id => $1;

/// Actual data of the object, can be anything.
T get data => $2;
}

// ignore: public_member_api_docs
extension SyncObjectsFind<T> on List<SyncObject<T>> {
// ignore: public_member_api_docs
SyncObject<T>? tryFind(final String id) => firstWhereOrNull((final 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:synchronize/src/conflict.dart';
import 'package:synchronize/src/object.dart';

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

/// Calculates the ETag of a given [object].
///
/// Should 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(final SyncObject<T1> object);

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

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

/// The sources the sync uses to sync from and to.
abstract interface class SyncSources<T1, T2> {
// ignore: public_member_api_docs
SyncSources(
this.sourceA,
this.sourceB,
);

// ignore: public_member_api_docs
final SyncSource<T1, T2> sourceA;

// ignore: public_member_api_docs
final SyncSource<T2, T1> sourceB;

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

0 comments on commit 218082a

Please sign in to comment.