mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 16:48:54 +01:00
crdt_sync_dart 0.12.1: serialize FileLogPersistence writes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
46ab26fa15
commit
6641d85f05
@ -7,8 +7,11 @@ import 'store.dart';
|
||||
/// Writes atomically -- to a per-process temp file, then `rename`d over the
|
||||
/// real path -- so a concurrent reader (e.g. a background sync isolate writing
|
||||
/// while the foreground app also writes) never observes a half-written file.
|
||||
/// The pid in the temp name keeps two writers from colliding. This generalizes
|
||||
/// diet_guard's hand-rolled `LogStorageService` write scheme.
|
||||
/// The pid in the temp name keeps two processes from colliding; within one
|
||||
/// instance, writes are serialized in issue order (last issued wins), so
|
||||
/// overlapping calls (e.g. per-keystroke saves) never share the temp file
|
||||
/// mid-flight. This generalizes diet_guard's hand-rolled `LogStorageService`
|
||||
/// write scheme.
|
||||
///
|
||||
/// Lives behind the `package:crdt_sync/crdt_sync_io.dart` entrypoint, not the
|
||||
/// main barrel, because it imports `dart:io` and so is unavailable on web --
|
||||
@ -20,6 +23,10 @@ class FileLogPersistence implements LogPersistence {
|
||||
|
||||
final File _file;
|
||||
|
||||
/// Completes when the most recently issued write has settled. Never
|
||||
/// rejects, so one failed write cannot block the ones queued after it.
|
||||
Future<void> _tail = Future<void>.value();
|
||||
|
||||
@override
|
||||
Future<String?> read() async {
|
||||
if (!_file.existsSync()) return null;
|
||||
@ -31,7 +38,14 @@ class FileLogPersistence implements LogPersistence {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String text) async {
|
||||
Future<void> write(String text) {
|
||||
final result = _tail.then((_) => _writeNow(text));
|
||||
// The caller still sees its own error via [result]; the chain ignores it.
|
||||
_tail = result.then((_) {}, onError: (Object _) {});
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _writeNow(String text) async {
|
||||
await _file.parent.create(recursive: true);
|
||||
final tmp = File('${_file.path}.$pid.tmp');
|
||||
await tmp.writeAsString(text);
|
||||
|
||||
@ -2,7 +2,7 @@ name: crdt_sync
|
||||
description: >-
|
||||
Shared CRDT merge scheme + GitHub-Contents-API sync transport for
|
||||
cross-device apps. Pure Dart, no Flutter dependency.
|
||||
version: 0.12.0
|
||||
version: 0.12.1
|
||||
publish_to: 'none'
|
||||
|
||||
environment:
|
||||
|
||||
@ -128,5 +128,32 @@ void main() {
|
||||
await store.write('{"hello":"world"}');
|
||||
expect(await store.read(), '{"hello":"world"}');
|
||||
});
|
||||
|
||||
test('50 overlapping writes all succeed and the last issued wins',
|
||||
() async {
|
||||
// Regression: every call shared one `<file>.<pid>.tmp`, so the first
|
||||
// rename moved it away and a later rename threw PathNotFoundException.
|
||||
final store = FileLogPersistence(File('${dir.path}/log.json'));
|
||||
await Future.wait([
|
||||
for (var i = 0; i < 50; i++) store.write('{"n":$i}'),
|
||||
]);
|
||||
expect(await store.read(), '{"n":49}');
|
||||
expect(dir.listSync().map((e) => e.uri.pathSegments.last),
|
||||
['log.json']);
|
||||
});
|
||||
|
||||
test('a failed write rejects its caller but does not block later writes',
|
||||
() async {
|
||||
final target = File('${dir.path}/log.json');
|
||||
// A directory at the target path makes the rename fail.
|
||||
Directory(target.path).createSync();
|
||||
final store = FileLogPersistence(target);
|
||||
await expectLater(
|
||||
store.write('{"n":0}'), throwsA(isA<FileSystemException>()));
|
||||
|
||||
Directory(target.path).deleteSync();
|
||||
await store.write('{"n":1}');
|
||||
expect(await store.read(), '{"n":1}');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user