fix: Fix sync once hanging forever when opened again too quickly after closing (#98)
This commit is contained in:
parent
d89118a43a
commit
d3b9a66efd
2 changed files with 79 additions and 3 deletions
|
|
@ -75,7 +75,12 @@ class CrdtSyncClient {
|
||||||
mode: CrdtSyncPeerMode.follower,
|
mode: CrdtSyncPeerMode.follower,
|
||||||
)
|
)
|
||||||
.listen(
|
.listen(
|
||||||
outboundChanges.addIfNotClosed,
|
(event) {
|
||||||
|
outboundChanges.addIfNotClosed(event);
|
||||||
|
if (once && event is CrdtSyncClose) {
|
||||||
|
unawaited(outboundChanges.closeOrSkip());
|
||||||
|
}
|
||||||
|
},
|
||||||
onDone: doneCompleter.completeOrSkip,
|
onDone: doneCompleter.completeOrSkip,
|
||||||
onError: doneCompleter.completeErrorOrSkip,
|
onError: doneCompleter.completeErrorOrSkip,
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
|
|
@ -117,8 +122,15 @@ extension on StreamController<CrdtSyncStreamEvent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeOrSkip() async {
|
Future<void> closeOrSkip() async {
|
||||||
if (isClosed) return;
|
if (!isClosed) {
|
||||||
await close();
|
unawaited(close());
|
||||||
|
}
|
||||||
|
// The done event is only delivered once the stream has a listener. If the
|
||||||
|
// method stream never listened (e.g. opening it failed), awaiting [done]
|
||||||
|
// would hang forever.
|
||||||
|
if (hasListener) {
|
||||||
|
await done;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,29 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'when an unauthenticated client calls syncOnce '
|
||||||
|
'then the unauthorized error is surfaced without waiting for stream teardown.',
|
||||||
|
() async {
|
||||||
|
final unauthenticatedClient = client.Client(
|
||||||
|
'http://localhost:${rawServerSession.server.port}',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
unauthenticatedClient.crdt
|
||||||
|
.syncOnce(clientSession)
|
||||||
|
.timeout(const Duration(seconds: 3)),
|
||||||
|
throwsA(
|
||||||
|
predicate<Object>(
|
||||||
|
(error) =>
|
||||||
|
error.toString() ==
|
||||||
|
'ServerpodClientException: Unauthorized, statusCode = 401',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'when client syncOnce is called repeatedly on a reused client '
|
'when client syncOnce is called repeatedly on a reused client '
|
||||||
'then the rounds complete promptly instead of stalling on teardown.',
|
'then the rounds complete promptly instead of stalling on teardown.',
|
||||||
|
|
@ -132,6 +155,27 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'when a running syncContinuously session is cancelled '
|
||||||
|
'then method stream teardown does not close the shared websocket.',
|
||||||
|
() async {
|
||||||
|
final stderr = await captureStderr(() async {
|
||||||
|
final syncSession = testClient.crdt.syncContinuously(clientSession);
|
||||||
|
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 300));
|
||||||
|
await syncSession.cancel();
|
||||||
|
await syncSession.done;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stderr, isNot(contains('WebSocketConnectionClosed')));
|
||||||
|
expect(
|
||||||
|
stderr,
|
||||||
|
isNot(contains('Message posted when web socket connection is closed')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'when client syncContinuously runs '
|
'when client syncContinuously runs '
|
||||||
'then neither side keeps sending frames while idle.',
|
'then neither side keeps sending frames while idle.',
|
||||||
|
|
@ -847,6 +891,26 @@ void main() {
|
||||||
await expectLater(syncSession.done, completes);
|
await expectLater(syncSession.done, completes);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'when a running syncContinuously session is cancelled '
|
||||||
|
'then method stream teardown does not close the shared websocket.',
|
||||||
|
() async {
|
||||||
|
final stderr = await captureStderr(() async {
|
||||||
|
final syncSession = testClient.crdt.syncContinuously(clientSession);
|
||||||
|
|
||||||
|
await syncSession.cancel();
|
||||||
|
await syncSession.done;
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stderr, isNot(contains('WebSocketConnectionClosed')));
|
||||||
|
expect(
|
||||||
|
stderr,
|
||||||
|
isNot(contains('Message posted when web socket connection is closed')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('and an inserted server person', () {
|
group('and an inserted server person', () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue