Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 23 additions & 30 deletions lib/providers/collab/strategy_op_queue_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,7 @@ class StrategyOpQueueNotifier extends Notifier<StrategyOpQueueState> {
state.successorByEntityKey,
);
var changed = false;
_storingMany = true;
try {
for (final key in keys) {
if (_awaitingRemoteAdoption.contains(key)) continue;
Expand Down Expand Up @@ -1589,9 +1590,11 @@ class StrategyOpQueueNotifier extends Notifier<StrategyOpQueueState> {
changed = true;
}
} catch (error, stackTrace) {
_storedMany();
_recordPersistenceFailure(error, stackTrace);
return;
}
_storedMany();
if (!changed) return;
_pruneCanvasOpIds();
final attentionMessage = _loadedAttentionMessage(
Expand Down Expand Up @@ -3675,41 +3678,31 @@ class StrategyOpQueueNotifier extends Notifier<StrategyOpQueueState> {
return Uri.decodeComponent(parts[1]);
}

/// Publishes the outbox as its records now stand: the account's summary
/// together with the open strategy's waiting work. Never one without the
/// other: a state that counted a newly stored change in the summary while
/// its waiting work still left it out read as nothing left to send, and
/// the canvas was redrawn from the server over the edit just stored.
///
/// While a page's work is being stored record by record ([_storingMany]),
/// it waits until all of it is, so a page of many items publishes once.
void _publishAccountSummary() {
if (_isDisposed) return;
final summary = _accountSummary(state.accountId);
final hasDurabilityFailure = _hasDurabilityFailureForCurrentAccount;
if (_sameAccountSummary(state.accountOutbox, summary) &&
state.hasDurabilityFailure == hasDurabilityFailure) {
if (_storingMany) {
_publishWaits = true;
return;
}
state = state.copyWith(
accountOutbox: summary,
hasDurabilityFailure: hasDurabilityFailure,
);
_refreshActiveQueueView();
}

bool _sameAccountSummary(
AccountStrategyOutboxSummary left,
AccountStrategyOutboxSummary right,
) {
if (left.accountId != right.accountId ||
left.strategies.length != right.strategies.length) {
return false;
}
for (final entry in left.strategies.entries) {
final other = right.strategies[entry.key];
if (other == null ||
other.queuedCount != entry.value.queuedCount ||
other.inFlightCount != entry.value.inFlightCount ||
other.pausedCount != entry.value.pausedCount ||
other.attentionCount != entry.value.attentionCount ||
other.successorCount != entry.value.successorCount ||
other.reason != entry.value.reason) {
return false;
}
}
return true;
bool _storingMany = false;
bool _publishWaits = false;

/// Ends [_storingMany], publishing if a publish waited for it.
void _storedMany() {
_storingMany = false;
if (!_publishWaits) return;
_publishWaits = false;
_refreshActiveQueueView();
}

Future<T> _serializeWrite<T>(Future<T> Function() action) {
Expand Down
48 changes: 48 additions & 0 deletions test/strategy_page_session_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9997,6 +9997,54 @@ void main() {
});
});

test(
"an edit made offline lands when a teammate's edit to another item "
'arrived first', () async {
final page = _page('page-1', 0);
server = _FakeServer(page.publicId, elements: [
_textElement(page.publicId, 'a', 'alpha', worldSized: true),
_textElement(page.publicId, 'c', 'charlie',
sortIndex: 1, worldSized: true),
])
..mergesByField = true;
final (container, _) = await openOnRealQueue(page);
for (var i = 0; i < 10; i++) {
await _settle();
}

container.read(_online.notifier).state = false;
container.read(textProvider.notifier).commitText('a', 'mine');
// The teammate's edit waits to be drawn until the user's is saved.
server.teammateEditElement('c', (data) => data['text'] = 'theirs');
showServer();
for (var i = 0; i < 4; i++) {
await _settle();
}

container.read(_online.notifier).state = true;
for (var round = 0; round < 3; round++) {
await container
.read(strategyProvider.notifier)
.forceSaveNow('cloud-strategy');
await container.read(strategyOpQueueProvider.notifier).flushNow();
for (var i = 0; i < 6; i++) {
await _settle();
}
showServer();
for (var i = 0; i < 6; i++) {
await _settle();
}
}

String? drawn(String id) => container
.read(textProvider)
.firstWhere((text) => text.id == id)
.text;
expect(cloudPayloadData(server.element('a').payload)['text'], 'mine');
expect(drawn('a'), 'mine');
expect(drawn('c'), 'theirs');
});

test('offline edits to a group nobody changed land on reconnect', () async {
final page = _page('page-1', 0);
server = _FakeServer(page.publicId, lineups: [fanIn(page.publicId)]);
Expand Down
Loading