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
7 changed files with 112 additions and 156 deletions

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);
}