mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 13:07:46 +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
|
/// 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
|
/// 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.
|
/// 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
|
/// The pid in the temp name keeps two processes from colliding; within one
|
||||||
/// diet_guard's hand-rolled `LogStorageService` write scheme.
|
/// 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
|
/// 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 --
|
/// main barrel, because it imports `dart:io` and so is unavailable on web --
|
||||||
@ -20,6 +23,10 @@ class FileLogPersistence implements LogPersistence {
|
|||||||
|
|
||||||
final File _file;
|
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
|
@override
|
||||||
Future<String?> read() async {
|
Future<String?> read() async {
|
||||||
if (!_file.existsSync()) return null;
|
if (!_file.existsSync()) return null;
|
||||||
@ -31,7 +38,14 @@ class FileLogPersistence implements LogPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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);
|
await _file.parent.create(recursive: true);
|
||||||
final tmp = File('${_file.path}.$pid.tmp');
|
final tmp = File('${_file.path}.$pid.tmp');
|
||||||
await tmp.writeAsString(text);
|
await tmp.writeAsString(text);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ name: crdt_sync
|
|||||||
description: >-
|
description: >-
|
||||||
Shared CRDT merge scheme + GitHub-Contents-API sync transport for
|
Shared CRDT merge scheme + GitHub-Contents-API sync transport for
|
||||||
cross-device apps. Pure Dart, no Flutter dependency.
|
cross-device apps. Pure Dart, no Flutter dependency.
|
||||||
version: 0.12.0
|
version: 0.12.1
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@ -128,5 +128,32 @@ void main() {
|
|||||||
await store.write('{"hello":"world"}');
|
await store.write('{"hello":"world"}');
|
||||||
expect(await store.read(), '{"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