chore: Publish version 0.0.1 of the packages (#103)

This commit is contained in:
Marcelo Mendonça Soares 2026-07-10 13:16:43 -03:00 • committed by GitHub
commit a24faf39af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 112 additions and 156 deletions

View file

@ -96,46 +96,22 @@ develop the app with normal repository calls against the wrapped `session`.
### 1. Add the package to the project ### 1. Add the package to the project
Since this package is still in development and not yet released to pub.dev, Add the packages to the server and client `pubspec.yaml` files.
you need to add it as a git reference to the project. First, add it normally
on the server and client `pubspec.yaml` files.
```yaml ```yaml
# your_project_client/pubspec.yaml # your_project_client/pubspec.yaml
dependencies: dependencies:
serverpod_offline_sync_client: 4.0.0-beta.0 serverpod_offline_sync_client: 0.0.1
``` ```
```yaml ```yaml
# your_project_server/pubspec.yaml # your_project_server/pubspec.yaml
dependencies: dependencies:
serverpod_offline_sync_server: 4.0.0-beta.0 serverpod_offline_sync_server: 0.0.1
``` ```
Then, on the workspace root `pubspec.yaml` file, add the following overrides: > [!NOTE]
> Version `0.0.1` requires Serverpod `4.0.0-beta.0`.
```yaml
dependency_overrides:
serverpod_offline_sync_client:
git:
url: https://github.com/marcelomendoncasoares/serverpod_offline_sync.git
path: packages/serverpod_offline_sync_client
ref: main
serverpod_offline_sync_server:
git:
url: https://github.com/marcelomendoncasoares/serverpod_offline_sync.git
path: packages/serverpod_offline_sync_server
ref: main
serverpod_offline_sync_shared:
git:
url: https://github.com/marcelomendoncasoares/serverpod_offline_sync.git
path: packages/serverpod_offline_sync_shared
ref: main
```
Be mindful that installing the package as a git reference to `main` is subject
to sudden changes when resolving dependencies. If you want to use a specific
version, pin the commit hash on the `ref` field instead.
After adding the dependencies, list the `serverpod_offline_sync` module on the After adding the dependencies, list the `serverpod_offline_sync` module on the
`generator.yaml` file: `generator.yaml` file:

View file

@ -12,10 +12,10 @@ dependencies:
intl: ^0.20.2 intl: ^0.20.2
path: ^1.9.1 path: ^1.9.1
serverpod_database: 4.0.0-beta.0 serverpod_database: 4.0.0-beta.0
serverpod_offline_sync:
path: ../packages/serverpod_offline_sync
serverpod_offline_sync_server: serverpod_offline_sync_server:
path: ../packages/serverpod_offline_sync_server path: ../packages/serverpod_offline_sync_server
serverpod_offline_sync_shared:
path: ../packages/serverpod_offline_sync_shared
serverpod_offline_sync_test_client: serverpod_offline_sync_test_client:
path: ../test/serverpod_offline_sync_test_client path: ../test/serverpod_offline_sync_test_client

View file

@ -7,8 +7,8 @@ import 'dart:typed_data';
import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:serverpod_database/serverpod_database.dart'; import 'package:serverpod_database/serverpod_database.dart';
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart'; import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
import 'package:serverpod_offline_sync_shared/serverpod_offline_sync_shared.dart';
import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_client.dart'; import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_client.dart';
import 'benchmark.dart'; import 'benchmark.dart';

View file

@ -44,8 +44,8 @@ dependency_overrides:
url: https://github.com/marcelomendoncasoares/serverpod.git url: https://github.com/marcelomendoncasoares/serverpod.git
path: packages/serverpod_database path: packages/serverpod_database
ref: fix-double-generator-bug ref: fix-double-generator-bug
serverpod_offline_sync_shared: serverpod_offline_sync:
path: ../../packages/serverpod_offline_sync_shared path: ../../packages/serverpod_offline_sync
serverpod_serialization: serverpod_serialization:
git: git:
url: https://github.com/marcelomendoncasoares/serverpod.git url: https://github.com/marcelomendoncasoares/serverpod.git

View file

@ -0,0 +1,13 @@
# Files and directories created by pub
.dart_tool/
.packages
# Omit committing pubspec.lock for library packages:
# https://dart.dev/guides/libraries/private-files#pubspeclock
pubspec.lock
# Conventional directory for build outputs
build/
# Directory created by dartdoc
doc/api/

View file

@ -0,0 +1 @@
../../CHANGELOG.md

View file

@ -0,0 +1,27 @@
Copyright 2025 The Serverpod authors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
4. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1 @@
../../README.md

View file

@ -0,0 +1,8 @@
include: ../../analysis_options.yaml
analyzer:
exclude:
# - lib/src/generated/**
formatter:
trailing_commas: preserve

View file

@ -0,0 +1,3 @@
export 'src/generated/protocol.dart';
export 'src/hlc/exceptions.dart';
export 'src/hlc/hlc.dart';

View file

@ -0,0 +1,8 @@
analyzer:
errors:
dead_code: ignore
duplicate_ignore: ignore
unnecessary_type_check: ignore
formatter:
trailing_commas: preserve

View file

@ -0,0 +1,66 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_serialization/serverpod_serialization.dart' as _i1;
/// Base class for HLC timestamps.
///
/// This class is used to represent the base HLC timestamp, which will be
/// extended with the node ID (in its canonical or normalized form).
class BaseHlc implements _i1.SerializableModel {
BaseHlc({
required this.hlcDatetime,
required this.hlcCounter,
});
factory BaseHlc.fromJson(Map<String, dynamic> jsonSerialization) {
return BaseHlc(
hlcDatetime: _i1.DateTimeJsonExtension.fromJson(
jsonSerialization['hlcDatetime'],
),
hlcCounter: jsonSerialization['hlcCounter'] as int,
);
}
/// The datetime component of the HLC timestamp.
DateTime hlcDatetime;
/// The counter component of the HLC timestamp.
int hlcCounter;
/// Returns a shallow copy of this [BaseHlc]
/// with some or all fields replaced by the given arguments.
@_i1.useResult
BaseHlc copyWith({
DateTime? hlcDatetime,
int? hlcCounter,
}) {
return BaseHlc(
hlcDatetime: hlcDatetime ?? this.hlcDatetime,
hlcCounter: hlcCounter ?? this.hlcCounter,
);
}
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'serverpod_offline_sync.BaseHlc',
'hlcDatetime': hlcDatetime.toJson(),
'hlcCounter': hlcCounter,
};
}
@override
String toString() {
return _i1.SerializationManager.encode(this);
}
}

View file

@ -0,0 +1,182 @@
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
/* To generate run: "serverpod generate" */
// ignore_for_file: implementation_imports
// ignore_for_file: library_private_types_in_public_api
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: public_member_api_docs
// ignore_for_file: type_literal_in_constant_pattern
// ignore_for_file: use_super_parameters
// ignore_for_file: invalid_use_of_internal_member
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:serverpod_serialization/serverpod_serialization.dart' as _i1;
import 'hlc/base.dart' as _i2;
export 'hlc/base.dart';
class Protocol extends _i1.SerializationManager {
Protocol._();
factory Protocol() => _instance;
static final Protocol _instance = Protocol._();
final Set<_i1.SerializationManager> _hostProtocols = {};
void registerHostProtocol(
String projectName,
_i1.SerializationManager protocol,
) {
_hostProtocols.add(protocol);
}
static String? getClassNameFromObjectJson(dynamic data) {
if (data is! Map) return null;
final className = data['__className__'] as String?;
if (className == null) return null;
if (!className.startsWith('serverpod_offline_sync.')) return className;
return className.substring(23);
}
@override
T deserialize<T>(
dynamic data, [
Type? t,
]) {
t ??= T;
final dataClassName = getClassNameFromObjectJson(data);
if (dataClassName != null && dataClassName != getClassNameForType(t)) {
try {
return deserializeByClassName({
'className': dataClassName,
'data': data,
});
} on FormatException catch (_) {
// If the className is not recognized (e.g., older client receiving
// data with a new subtype), fall back to deserializing without the
// className, using the expected type T.
}
}
if (t == _i2.BaseHlc) {
return _i2.BaseHlc.fromJson(data) as T;
}
if (t == _i1.getType<_i2.BaseHlc?>()) {
return (data != null ? _i2.BaseHlc.fromJson(data) : null) as T;
}
return super.deserialize<T>(data, t);
}
static String? getClassNameForType(Type type) {
return switch (type) {
_i2.BaseHlc => 'BaseHlc',
_ => null,
};
}
@override
String? getClassNameForObject(Object? data) {
String? className = super.getClassNameForObject(data);
if (className != null) return className;
if (data is Map<String, dynamic> && data['__className__'] is String) {
return (data['__className__'] as String).replaceFirst(
'serverpod_offline_sync.',
'',
);
}
switch (data) {
case _i2.BaseHlc():
return 'BaseHlc';
}
return null;
}
@override
dynamic deserializeByClassName(Map<String, dynamic> data) {
var dataClassName = data['className'];
if (dataClassName is! String) {
return super.deserializeByClassName(data);
}
if (dataClassName == 'BaseHlc') {
return deserialize<_i2.BaseHlc>(data['data']);
}
return super.deserializeByClassName(data);
}
@override
Object? dynamicFieldToJson(
Object? object, {
bool forProtocol = false,
}) {
if ((object is List || object is Set || object is Map) ||
getClassNameForObject(object) != null) {
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
}
for (final protocol in _hostProtocols) {
final className = protocol.getClassNameForObject(object);
if (className == null) continue;
final host = protocol.getModuleName();
final wrapped = {
'className': className.contains('.') ? className : '$host.$className',
'data': object,
};
return forProtocol
? _i1.SerializationManager.toEncodableForProtocol(wrapped)
: _i1.SerializationManager.toEncodable(wrapped);
}
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
}
@override
dynamic deserializeDynamicFieldValue(Object? value) {
if (value == null) return null;
if (value is! Map<String, dynamic> || value['className'] is! String) {
throw FormatException(
'Dynamic fields are encoded as a Map with className and data, but got '
'${value.runtimeType} instead.',
);
}
final className = value['className'] as String;
for (final protocol in _hostProtocols) {
final host = protocol.getModuleName();
final hostPrefix = '$host.';
if (className.startsWith(hostPrefix)) {
final strippedClassName = className.substring(hostPrefix.length);
if (strippedClassName.contains('.')) {
throw FormatException(
'Dynamic field className must not use multiple prefixes: $className',
);
}
final hostData = Map<String, dynamic>.from(value);
hostData['className'] = strippedClassName;
return protocol.deserializeByClassName(hostData);
}
}
if (className.contains('.')) {
for (final protocol in _hostProtocols) {
try {
return protocol.deserializeByClassName(value);
} on FormatException catch (_) {}
}
}
return deserializeByClassName(value);
}
@override
String getModuleName() => 'serverpod_offline_sync';
/// Maps any `Record`s known to this [Protocol] to their JSON representation
///
/// Throws in case the record type is not known.
///
/// This method will return `null` (only) for `null` inputs.
Map<String, dynamic>? mapRecordToJson(Record? record) {
if (record == null) {
return null;
}
throw Exception('Unsupported record type ${record.runtimeType}');
}
}

View file

@ -0,0 +1,11 @@
### Base class for HLC timestamps.
###
### This class is used to represent the base HLC timestamp, which will be
### extended with the node ID (in its canonical or normalized form).
class: BaseHlc
fields:
### The datetime component of the HLC timestamp.
hlcDatetime: DateTime
### The counter component of the HLC timestamp.
hlcCounter: int

View file

@ -0,0 +1,41 @@
import 'package:serverpod_serialization/serverpod_serialization.dart';
/// Exception thrown when the clock drift exceeds the maximum allowed.
class ClockDriftException implements Exception {
/// Creates a new instance of [ClockDriftException].
ClockDriftException(DateTime dateTime, DateTime wallTime, this.maxDrift)
: drift = dateTime.difference(wallTime);
/// The duration of the clock drift.
final Duration drift;
/// The maximum allowed clock drift.
final Duration maxDrift;
@override
String toString() => 'Clock drift of $drift ms exceeds maximum ($maxDrift)';
}
/// Exception thrown when the timestamp counter overflows.
class OverflowException implements Exception {
/// Creates a new instance of [OverflowException].
OverflowException(this.counter);
/// The counter that overflowed.
final int counter;
@override
String toString() => 'Timestamp counter overflow: $counter';
}
/// Exception thrown when a duplicate node is detected.
class DuplicateNodeException implements Exception {
/// Creates a new instance of [DuplicateNodeException].
DuplicateNodeException(this.nodeId);
/// The node ID that is duplicated.
final UuidValue nodeId;
@override
String toString() => 'Duplicate node: $nodeId';
}

View file

@ -0,0 +1,204 @@
// This file includes code from the project: https://github.com/cachapa/crdt
// Licensed under Apache License 2.0: https://www.apache.org/licenses/LICENSE-2.0
// Copyright Daniel Cachapa.
//
// Modifications copyright 2026 Marcelo Soares
// Licensed under MIT License: https://opensource.org/licenses/MIT
import 'package:clock/clock.dart';
import 'package:serverpod_serialization/serverpod_serialization.dart';
import '../../src/generated/hlc/base.dart';
import 'exceptions.dart';
/// A Hybrid Logical Clock implementation that supports both ISO 8601 and Unix
/// timestamp formats. The datetime component has millisecond precision and is
/// always stored in UTC.
///
/// Although the original HLC paper uses microsecond precision, since it was
/// proved that the counter typically stays in single digits, millisecond
/// precision can be considered enough for the vast majority of applications.
///
/// The advantage of using milliseconds is that the timestamp can be generated
/// on any database (inside SQLite3 for example) and will occupy less space in
/// the database.
///
/// Inspiration: https://cse.buffalo.edu/tech-reports/2014-04.pdf
@immutable
class Hlc implements Comparable<Hlc> {
/// Creates a new instance of [Hlc].
///
/// Precision of the [datetime] is milliseconds.
factory Hlc(DateTime datetime, int counter, UuidValue nodeId) =>
Hlc._(datetime.toUtcMillisecond(), counter, nodeId);
/// Parse an HLC string in the format `<ISO8601_date>-<counter>-<node_id>`
/// or `<unix_timestamp>-<counter>-<node_id>`.
///
/// For backward compatibility, the node segment may use a legacy
/// `uuid:worker_id` form; the worker suffix is ignored.
factory Hlc.parse(String timestamp) {
final indexOfColon = timestamp.indexOf(':');
final indexOfStart = indexOfColon > 0 && indexOfColon < 20 ? indexOfColon : 0;
final counterDash = timestamp.indexOf('-', indexOfStart);
final timestampPart = timestamp.substring(0, counterDash);
final nodeIdDash = timestamp.indexOf('-', counterDash + 1);
final counterPart = timestamp.substring(counterDash + 1, nodeIdDash);
final unixTimestamp = int.tryParse(timestampPart);
final datetime = unixTimestamp != null
? DateTime.fromMillisecondsSinceEpoch(unixTimestamp, isUtc: true)
: DateTime.tryParse(timestampPart);
if (datetime == null) {
throw FormatException('Invalid HLC string: $timestamp');
}
final counter = int.parse(counterPart, radix: 16);
final nodeIdPart = timestamp.substring(nodeIdDash + 1);
return Hlc(datetime, counter, parseNodeSegment(nodeIdPart));
}
/// Convenience method to decode the HLC from a JSON map.
factory Hlc.fromJson(Map<String, dynamic> json) => Hlc(
DateTime.parse(json['datetime'] as String),
int.parse(json['counter'] as String, radix: 16),
UuidValue.withValidation(json['node'] as String),
);
const Hlc._(this.datetime, this.counter, this.nodeId)
: assert(counter <= _maxCounter);
/// Instantiates an Hlc at the beginning of time and space: January 1, 1970.
factory Hlc.zero(UuidValue nodeId) => Hlc(DateTime.utc(1970), 0, nodeId);
/// Instantiates an Hlc using the wall clock.
factory Hlc.now(UuidValue nodeId) => Hlc(DateTime.now(), 0, nodeId);
/// Parses the node segment of an HLC string: a UUID, or legacy `uuid:workerId`.
static UuidValue parseNodeSegment(String segment) {
final parts = segment.split(':');
if (parts.length == 2 && int.tryParse(parts[1]) != null) {
return UuidValue.withValidation(parts[0]);
}
return UuidValue.withValidation(segment);
}
/// The date and time of the HLC.
final DateTime datetime;
/// The counter of the HLC.
final int counter;
/// The logical node ID for this HLC.
final UuidValue nodeId;
/// The Unix timestamp of the HLC.
int get unixTimestamp => datetime.millisecondsSinceEpoch;
static const _maxDrift = Duration(minutes: 1);
static const _maxCounter = 0xFFFF;
/// Increments the current timestamp for transmission to another system.
Hlc increment() {
final localWallTime = clock.now().toUtc();
final datetimeNew = localWallTime.isAfter(datetime) ? localWallTime : datetime;
final counterNew = datetimeNew == datetime ? counter + 1 : 0;
if (datetimeNew.difference(localWallTime) > const Duration(minutes: 1)) {
throw ClockDriftException(datetimeNew, localWallTime, _maxDrift);
}
if (counterNew > _maxCounter) {
throw OverflowException(counterNew);
}
return Hlc(datetimeNew, counterNew, nodeId);
}
/// Compares and validates a timestamp from a remote system with the local
/// timestamp to preserve monotonicity.
Hlc merge(Hlc remote) {
if (remote.datetime.isBefore(datetime) ||
(remote.datetime.isAtSameMomentAs(datetime) && remote.counter <= counter)) {
return this;
}
if (nodeId == remote.nodeId) {
throw DuplicateNodeException(nodeId);
}
final localWallTime = clock.now().toUtc();
if (remote.datetime.difference(localWallTime) > _maxDrift) {
throw ClockDriftException(remote.datetime, localWallTime, _maxDrift);
}
return remote.copyWith(nodeId: nodeId);
}
/// Create a copy of this object replacing the optional properties.
Hlc copyWith({DateTime? datetime, int? counter, UuidValue? nodeId}) => Hlc(
datetime ?? this.datetime,
counter ?? this.counter,
nodeId ?? this.nodeId,
);
/// Returns the maximum HLC between the two given HLCs.
Hlc maxBetween(Hlc? other) => other == null || this > other ? this : other;
@override
String toString() =>
'${datetime.toIso8601String()}-'
'${counter.toRadixString(16).toUpperCase().padLeft(4, '0')}-'
'$nodeId';
/// Convenience method for easy json encoding.
Map<String, dynamic> toJson() => {
'datetime': datetime.toIso8601String(),
'counter': counter.toRadixString(16).toUpperCase().padLeft(4, '0'),
'node': nodeId.toString(),
};
@override
int get hashCode => toString().hashCode;
@override
bool operator ==(Object other) => other is Hlc && compareTo(other) == 0;
/// Whether this [Hlc] is causally before the [other].
bool operator <(Object other) => other is Hlc && compareTo(other) < 0;
/// Whether this [Hlc] is causally before or equal to the [other].
bool operator <=(Object other) => this < other || this == other;
/// Whether this [Hlc] is causally after the [other].
bool operator >(Object other) => other is Hlc && compareTo(other) > 0;
/// Whether this [Hlc] is causally after or equal to the [other].
bool operator >=(Object other) => this > other || this == other;
@override
int compareTo(Hlc other) => datetime.isAtSameMomentAs(other.datetime)
? counter == other.counter
? nodeId.uuid.compareTo(other.nodeId.uuid)
: counter - other.counter
: datetime.compareTo(other.datetime);
}
extension on DateTime {
DateTime toUtcMillisecond() =>
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch, isUtc: true);
}
/// Extensions for [BaseHlc] to convert it to an [Hlc].
extension BaseHlcToHlc on BaseHlc {
/// Converts the [BaseHlc] to an [Hlc] with the given [nodeId].
Hlc toHlcForNode(UuidValue nodeId) => Hlc(hlcDatetime, hlcCounter, nodeId);
}
/// Extensions for an [Iterable] of [Hlc].
extension HlcMax on Iterable<Hlc> {
/// The greatest HLC represented by the HLCs in this iterable.
Hlc get max => reduce((a, b) => a > b ? a : b);
}

View file

@ -0,0 +1,19 @@
name: serverpod_offline_sync
version: 0.0.1
description: Shared package for the Serverpod Offline Sync project.
homepage: https://github.com/marcelomendoncasoares/serverpod_offline_sync
environment:
sdk: '^3.10.3'
resolution: workspace
dependencies:
clock: ^1.1.2
serverpod_database: 4.0.0-beta.0
serverpod_serialization: 4.0.0-beta.0
uuid: ^4.5.3
dev_dependencies:
lints: '>=3.0.0 <7.0.0'
test: ^1.25.5

View file

@ -0,0 +1,129 @@
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
final _dateTime = DateTime.parse('2001-09-09T01:46:40.000Z');
final _nodeId = const Uuid().v4obj();
void main() {
group('Given two Hlcs with same dateTime, counter and nodeId when comparing ', () {
final hlc1 = Hlc(_dateTime, 17, _nodeId);
final hlc2 = Hlc(_dateTime, 17, _nodeId);
test('then they are equal.', () {
expect(hlc1, hlc2);
expect(hlc1 == hlc2, isTrue);
expect(hlc1, equals(hlc2));
});
test('then compareTo returns 0.', () {
expect(hlc1.compareTo(hlc2), 0);
});
test('then <= is true.', () {
expect(hlc1 <= hlc2, isTrue);
expect(hlc1, lessThanOrEqualTo(hlc2));
});
test('then >= is true.', () {
expect(hlc1 >= hlc2, isTrue);
expect(hlc1, greaterThanOrEqualTo(hlc2));
});
});
group(
'Given two Hlcs with same dateTime and counter and different nodeId when comparing ',
() {
final node2 = const Uuid().v4obj();
final hlc1 = Hlc(_dateTime, 17, _nodeId);
final hlc2 = Hlc(_dateTime, 17, node2);
test('then they are not equal.', () {
expect(hlc1, isNot(hlc2));
});
test('then compareTo follows lexicographic node uuid order.', () {
expect(
hlc1.compareTo(hlc2),
hlc1.nodeId.uuid.compareTo(hlc2.nodeId.uuid),
);
});
test('then one is greater or less by uuid order.', () {
if (_nodeId.uuid.compareTo(node2.uuid) > 0) {
expect(hlc1 > hlc2, isTrue);
} else {
expect(hlc1 < hlc2, isTrue);
}
});
},
);
group('Given two Hlcs with same dateTime and different counter when comparing ', () {
final hlc1 = Hlc(_dateTime, 17, _nodeId);
final hlc2 = Hlc(_dateTime, 18, _nodeId);
test('then they are not equal.', () {
expect(hlc1, isNot(hlc2));
});
test('then compareTo returns -1.', () {
expect(hlc1.compareTo(hlc2), -1);
});
test('then < is true.', () {
expect(hlc1 < hlc2, isTrue);
expect(hlc1, lessThan(hlc2));
});
test('then <= is true.', () {
expect(hlc1 <= hlc2, isTrue);
expect(hlc1, lessThanOrEqualTo(hlc2));
});
test('then > is false.', () {
expect(hlc1 > hlc2, isFalse);
});
test('then >= is false.', () {
expect(hlc1 >= hlc2, isFalse);
expect(hlc1, isNot(greaterThanOrEqualTo(hlc2)));
});
});
group(
'Given two Hlcs with different dateTime when comparing ',
() {
final hlc1 = Hlc(_dateTime, 0, _nodeId);
final hlc2 = Hlc(_dateTime.add(const Duration(milliseconds: 1)), 17, _nodeId);
test('then they are not equal.', () {
expect(hlc1, isNot(hlc2));
});
test('then compareTo returns -1.', () {
expect(hlc1.compareTo(hlc2), -1);
});
test('then < is true.', () {
expect(hlc1 < hlc2, isTrue);
expect(hlc1, lessThan(hlc2));
});
test('then <= is true.', () {
expect(hlc1 <= hlc2, isTrue);
expect(hlc1, lessThanOrEqualTo(hlc2));
});
test('then > is false.', () {
expect(hlc1 > hlc2, isFalse);
});
test('then >= is false.', () {
expect(hlc1 >= hlc2, isFalse);
expect(hlc1, isNot(greaterThanOrEqualTo(hlc2)));
});
},
);
}

View file

@ -0,0 +1,32 @@
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
final _dateTime = DateTime.parse('2001-09-09T01:46:40.000Z');
final _nodeId = const Uuid().v4obj();
void main() {
group('Given an HLC', () {
final hlc = Hlc(_dateTime, 17, _nodeId);
test('when calling copyWith with new dateTime then result has new dateTime.', () {
final newDateTime = _dateTime.add(const Duration(minutes: 1));
final newHlc = hlc.copyWith(datetime: newDateTime);
expect(newHlc.datetime, newDateTime);
});
test('when calling copyWith with new counter then result has new counter.', () {
final newHlc = hlc.copyWith(counter: 18);
expect(newHlc.counter, 18);
});
test('when calling copyWith with new nodeId then result has new nodeId.', () {
final newNodeId = const Uuid().v4obj();
final newHlc = hlc.copyWith(nodeId: newNodeId);
expect(newHlc.nodeId, newNodeId);
});
});
}

View file

@ -0,0 +1,84 @@
import 'package:clock/clock.dart';
import 'package:serverpod_offline_sync/serverpod_offline_sync.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
final _dateTime = DateTime.parse('2001-09-09T01:46:40.000Z');
final _nodeId = const Uuid().v4obj();
void main() {
group('Given an HLC with lower canonical time than wall time', () {
final hlc = Hlc(_dateTime, 17, _nodeId);
final wallTime = _dateTime.advance();
test('when incrementing then dateTime becomes wall time and counter resets.', () {
withClock(Clock.fixed(wallTime), () {
final sendHlc = hlc.increment();
expect(sendHlc, isNot(hlc));
expect(sendHlc.datetime, wallTime);
expect(sendHlc.counter, 0);
expect(sendHlc.nodeId, hlc.nodeId);
});
});
});
group('Given an HLC with equal canonical time and wall time', () {
final hlc = Hlc(_dateTime, 17, _nodeId);
final wallTime = _dateTime;
test('when incrementing then counter increments and dateTime is unchanged.', () {
withClock(Clock.fixed(wallTime), () {
final sendHlc = hlc.increment();
expect(sendHlc, isNot(hlc));
expect(sendHlc.datetime, hlc.datetime);
expect(sendHlc.counter, 18);
expect(sendHlc.nodeId, hlc.nodeId);
});
});
});
group('Given an HLC with higher canonical time than wall time', () {
final hlc = Hlc(_dateTime, 17, _nodeId);
final wallTime = _dateTime.retreat();
test('when incrementing then counter increments and dateTime is unchanged.', () {
withClock(Clock.fixed(wallTime), () {
final sendHlc = hlc.increment();
expect(sendHlc, isNot(hlc));
expect(sendHlc.datetime, hlc.datetime);
expect(sendHlc.counter, 18);
expect(sendHlc.nodeId, hlc.nodeId);
});
});
});
group('Given an HLC with canonical time more than one minute ahead of wall time', () {
final hlc = Hlc(_dateTime.add(const Duration(minutes: 1, seconds: 5)), 0, _nodeId);
final wallTime = _dateTime;
test('when incrementing then ClockDriftException is thrown.', () {
withClock(Clock.fixed(wallTime), () {
expect(hlc.increment, throwsA(isA<ClockDriftException>()));
});
});
});
group('Given an HLC with counter at maximum and canonical time at wall time', () {
final hlc = Hlc(_dateTime, 0xFFFF, _nodeId);
final wallTime = _dateTime;
test('when incrementing at same time then OverflowException is thrown.', () {
withClock(Clock.fixed(wallTime), () {
expect(hlc.increment, throwsA(isA<OverflowException>()));
});
});
});
}
extension on DateTime {
DateTime advance() => add(const Duration(milliseconds: 1));
DateTime retreat() => subtract(const Duration(milliseconds: 1));
}

Some files were not shown because too many files have changed in this diff Show more