feat: Factor out "multiple-waiting" into its own thing
This commit is contained in:
parent
7e588f01b0
commit
47337540f5
67
packages/moxxmpp/lib/src/util/wait.dart
Normal file
67
packages/moxxmpp/lib/src/util/wait.dart
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
|
/// This class allows for multiple asynchronous code places to wait on the
|
||||||
|
/// same computation of type [V], indentified by a key of type [K].
|
||||||
|
class WaitForTracker<K, V> {
|
||||||
|
/// The mapping of key -> Completer for the pending tasks.
|
||||||
|
final Map<K, List<Completer<V>>> _tracker = {};
|
||||||
|
|
||||||
|
/// The lock for accessing _tracker.
|
||||||
|
final Lock _lock = Lock();
|
||||||
|
|
||||||
|
/// Wait for a task with key [key]. If there was no such task already
|
||||||
|
/// present, returns null. If one or more tasks were already present, returns
|
||||||
|
/// a future that will resolve to the result of the first task.
|
||||||
|
Future<Future<V>?> waitFor(K key) async {
|
||||||
|
final result = await _lock.synchronized(() {
|
||||||
|
if (_tracker.containsKey(key)) {
|
||||||
|
// The task already exists. Just append outselves
|
||||||
|
final completer = Completer<V>();
|
||||||
|
_tracker[key]!.add(completer);
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The task does not exist yet
|
||||||
|
_tracker[key] = List<Completer<V>>.empty(growable: true);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result?.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a task with key [key] to [value].
|
||||||
|
Future<void> resolve(K key, V value) async {
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
if (!_tracker.containsKey(key)) return;
|
||||||
|
|
||||||
|
for (final completer in _tracker[key]!) {
|
||||||
|
completer.complete(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tracker.remove(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resolveAll(V value) async {
|
||||||
|
await _lock.synchronized(() {
|
||||||
|
for (final key in _tracker.keys) {
|
||||||
|
for (final completer in _tracker[key]!) {
|
||||||
|
completer.complete(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove all tasks from the tracker.
|
||||||
|
Future<void> clear() async {
|
||||||
|
await _lock.synchronized(_tracker.clear);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool hasTasksRunning() => _tracker.isNotEmpty;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
List<Completer<V>> getRunningTasks(K key) => _tracker[key]!;
|
||||||
|
}
|
23
packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart
Normal file
23
packages/moxxmpp/lib/src/xeps/xep_0030/cache.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
@internal
|
||||||
|
@immutable
|
||||||
|
class DiscoCacheKey {
|
||||||
|
const DiscoCacheKey(this.jid, this.node);
|
||||||
|
|
||||||
|
/// The JID we're requesting disco data from.
|
||||||
|
final String jid;
|
||||||
|
|
||||||
|
/// Optionally the node we are requesting from.
|
||||||
|
final String? node;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is DiscoCacheKey &&
|
||||||
|
jid == other.jid &&
|
||||||
|
node == other.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => jid.hashCode ^ node.hashCode;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:moxxmpp/src/connection.dart';
|
||||||
import 'package:moxxmpp/src/events.dart';
|
import 'package:moxxmpp/src/events.dart';
|
||||||
import 'package:moxxmpp/src/jid.dart';
|
import 'package:moxxmpp/src/jid.dart';
|
||||||
import 'package:moxxmpp/src/managers/base.dart';
|
import 'package:moxxmpp/src/managers/base.dart';
|
||||||
@ -10,6 +11,8 @@ import 'package:moxxmpp/src/namespaces.dart';
|
|||||||
import 'package:moxxmpp/src/stanza.dart';
|
import 'package:moxxmpp/src/stanza.dart';
|
||||||
import 'package:moxxmpp/src/stringxml.dart';
|
import 'package:moxxmpp/src/stringxml.dart';
|
||||||
import 'package:moxxmpp/src/types/result.dart';
|
import 'package:moxxmpp/src/types/result.dart';
|
||||||
|
import 'package:moxxmpp/src/util/wait.dart';
|
||||||
|
import 'package:moxxmpp/src/xeps/xep_0030/cache.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/errors.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/helpers.dart';
|
||||||
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
import 'package:moxxmpp/src/xeps/xep_0030/types.dart';
|
||||||
@ -22,21 +25,6 @@ typedef DiscoInfoRequestCallback = Future<DiscoInfo> Function();
|
|||||||
/// Callback that is called when a disco#items requests is received on a given node.
|
/// Callback that is called when a disco#items requests is received on a given node.
|
||||||
typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
|
typedef DiscoItemsRequestCallback = Future<List<DiscoItem>> Function();
|
||||||
|
|
||||||
@immutable
|
|
||||||
class DiscoCacheKey {
|
|
||||||
const DiscoCacheKey(this.jid, this.node);
|
|
||||||
final String jid;
|
|
||||||
final String? node;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is DiscoCacheKey && jid == other.jid && node == other.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => jid.hashCode ^ node.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This manager implements XEP-0030 by providing a way of performing disco#info and
|
/// This manager implements XEP-0030 by providing a way of performing disco#info and
|
||||||
/// disco#items requests and answering those requests.
|
/// disco#items requests and answering those requests.
|
||||||
/// A caching mechanism is also provided.
|
/// A caching mechanism is also provided.
|
||||||
@ -62,8 +50,11 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
/// Map full JID to Disco Info
|
/// Map full JID to Disco Info
|
||||||
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
final Map<DiscoCacheKey, DiscoInfo> _discoInfoCache = {};
|
||||||
|
|
||||||
/// Mapping the full JID to a list of running requests
|
/// The tracker for tracking disco#info queries that are in flight.
|
||||||
final Map<DiscoCacheKey, List<Completer<Result<DiscoError, DiscoInfo>>>> _runningInfoQueries = {};
|
final WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> _discoInfoTracker = WaitForTracker();
|
||||||
|
|
||||||
|
/// The tracker for tracking disco#info queries that are in flight.
|
||||||
|
final WaitForTracker<DiscoCacheKey, Result<DiscoError, List<DiscoItem>>> _discoItemsTracker = WaitForTracker();
|
||||||
|
|
||||||
/// Cache lock
|
/// Cache lock
|
||||||
final Lock _cacheLock = Lock();
|
final Lock _cacheLock = Lock();
|
||||||
@ -81,10 +72,7 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
List<String> get features => _features;
|
List<String> get features => _features;
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
bool hasInfoQueriesRunning() => _runningInfoQueries.isNotEmpty;
|
WaitForTracker<DiscoCacheKey, Result<DiscoError, DiscoInfo>> get infoTracker => _discoInfoTracker;
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
List<Completer<Result<DiscoError, DiscoInfo>>> getRunningInfoQueries(DiscoCacheKey key) => _runningInfoQueries[key]!;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
List<StanzaHandler> getIncomingStanzaHandlers() => [
|
||||||
@ -118,7 +106,21 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
Future<void> onXmppEvent(XmppEvent event) async {
|
Future<void> onXmppEvent(XmppEvent event) async {
|
||||||
if (event is PresenceReceivedEvent) {
|
if (event is PresenceReceivedEvent) {
|
||||||
await _onPresence(event.jid, event.presence);
|
await _onPresence(event.jid, event.presence);
|
||||||
} else if (event is StreamResumeFailedEvent) {
|
} else if (event is ConnectionStateChangedEvent) {
|
||||||
|
// TODO(Unknown): This handling is stupid. We should have an event that is
|
||||||
|
// triggered when we cannot guarantee that everything is as
|
||||||
|
// it was before.
|
||||||
|
if (event.state != XmppConnectionState.connected) return;
|
||||||
|
if (event.resumed) return;
|
||||||
|
|
||||||
|
// Cancel all waiting requests
|
||||||
|
await _discoInfoTracker.resolveAll(
|
||||||
|
Result<DiscoError, DiscoInfo>(UnknownDiscoError()),
|
||||||
|
);
|
||||||
|
await _discoItemsTracker.resolveAll(
|
||||||
|
Result<DiscoError, List<DiscoItem>>(UnknownDiscoError()),
|
||||||
|
);
|
||||||
|
|
||||||
await _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
// Clear the cache
|
// Clear the cache
|
||||||
_discoInfoCache.clear();
|
_discoInfoCache.clear();
|
||||||
@ -259,46 +261,37 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
|
Future<void> _exitDiscoInfoCriticalSection(DiscoCacheKey key, Result<DiscoError, DiscoInfo> result) async {
|
||||||
return _cacheLock.synchronized(() async {
|
await _cacheLock.synchronized(() async {
|
||||||
// Complete all futures
|
|
||||||
for (final completer in _runningInfoQueries[key]!) {
|
|
||||||
completer.complete(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to cache if it is a result
|
// Add to cache if it is a result
|
||||||
if (result.isType<DiscoInfo>()) {
|
if (result.isType<DiscoInfo>()) {
|
||||||
_discoInfoCache[key] = result.get<DiscoInfo>();
|
_discoInfoCache[key] = result.get<DiscoInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from the request cache
|
|
||||||
_runningInfoQueries.remove(key);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await _discoInfoTracker.resolve(key, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
/// Sends a disco info query to the (full) jid [entity], optionally with node=[node].
|
||||||
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
|
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
|
||||||
final cacheKey = DiscoCacheKey(entity, node);
|
final cacheKey = DiscoCacheKey(entity, node);
|
||||||
DiscoInfo? info;
|
DiscoInfo? info;
|
||||||
Completer<Result<DiscoError, DiscoInfo>>? completer;
|
final ffuture = await _cacheLock.synchronized<Future<Future<Result<DiscoError, DiscoInfo>>?>?>(() async {
|
||||||
await _cacheLock.synchronized(() async {
|
|
||||||
// Check if we already know what the JID supports
|
// Check if we already know what the JID supports
|
||||||
if (_discoInfoCache.containsKey(cacheKey)) {
|
if (_discoInfoCache.containsKey(cacheKey)) {
|
||||||
info = _discoInfoCache[cacheKey];
|
info = _discoInfoCache[cacheKey];
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
// Is a request running?
|
return _discoInfoTracker.waitFor(cacheKey);
|
||||||
if (_runningInfoQueries.containsKey(cacheKey)) {
|
|
||||||
completer = Completer();
|
|
||||||
_runningInfoQueries[cacheKey]!.add(completer!);
|
|
||||||
} else {
|
|
||||||
_runningInfoQueries[cacheKey] = List.from(<Completer<DiscoInfo?>>[]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
return Result<DiscoError, DiscoInfo>(info);
|
return Result<DiscoError, DiscoInfo>(info);
|
||||||
} else if (completer != null) {
|
} else {
|
||||||
return completer!.future;
|
final future = await ffuture;
|
||||||
|
if (future != null) {
|
||||||
|
return future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final stanza = await getAttributes().sendStanza(
|
final stanza = await getAttributes().sendStanza(
|
||||||
@ -331,6 +324,12 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
|
|
||||||
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
|
/// Sends a disco items query to the (full) jid [entity], optionally with node=[node].
|
||||||
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
|
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(String entity, { String? node, bool shouldEncrypt = true }) async {
|
||||||
|
final key = DiscoCacheKey(entity, node);
|
||||||
|
final future = await _discoItemsTracker.waitFor(key);
|
||||||
|
if (future != null) {
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
final stanza = await getAttributes()
|
final stanza = await getAttributes()
|
||||||
.sendStanza(
|
.sendStanza(
|
||||||
buildDiscoItemsQueryStanza(entity, node: node),
|
buildDiscoItemsQueryStanza(entity, node: node),
|
||||||
@ -338,12 +337,18 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
) as Stanza;
|
) as Stanza;
|
||||||
|
|
||||||
final query = stanza.firstTag('query');
|
final query = stanza.firstTag('query');
|
||||||
if (query == null) return Result(InvalidResponseDiscoError());
|
if (query == null) {
|
||||||
|
final result = Result<DiscoError, List<DiscoItem>>(InvalidResponseDiscoError());
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
if (stanza.type == 'error') {
|
if (stanza.type == 'error') {
|
||||||
//final error = stanza.firstTag('error');
|
//final error = stanza.firstTag('error');
|
||||||
//print("Disco Items error: " + error.toXml());
|
//print("Disco Items error: " + error.toXml());
|
||||||
return Result(ErrorResponseDiscoError());
|
final result = Result<DiscoError, List<DiscoItem>>(ErrorResponseDiscoError());
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
final items = query.findTags('item').map((node) => DiscoItem(
|
final items = query.findTags('item').map((node) => DiscoItem(
|
||||||
@ -352,7 +357,9 @@ class DiscoManager extends XmppManagerBase {
|
|||||||
name: node.attributes['name'] as String?,
|
name: node.attributes['name'] as String?,
|
||||||
),).toList();
|
),).toList();
|
||||||
|
|
||||||
return Result(items);
|
final result = Result<DiscoError, List<DiscoItem>>(items);
|
||||||
|
await _discoItemsTracker.resolve(key, result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries information about a jid based on its node and capability hash.
|
/// Queries information about a jid based on its node and capability hash.
|
||||||
|
40
packages/moxxmpp/test/wait_test.dart
Normal file
40
packages/moxxmpp/test/wait_test.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'package:test/test.dart';
|
||||||
|
import 'package:moxxmpp/src/util/wait.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Test adding and resolving', () async {
|
||||||
|
// ID -> Milliseconds since epoch
|
||||||
|
final tracker = WaitForTracker<int, int>();
|
||||||
|
|
||||||
|
int r2 = 0;
|
||||||
|
int r3 = 0;
|
||||||
|
|
||||||
|
// Queue some jobs
|
||||||
|
final r1 = await tracker.waitFor(0);
|
||||||
|
expect(r1, null);
|
||||||
|
|
||||||
|
tracker
|
||||||
|
.waitFor(0)
|
||||||
|
.then((result) async {
|
||||||
|
expect(result != null, true);
|
||||||
|
r2 = await result!;
|
||||||
|
});
|
||||||
|
tracker
|
||||||
|
.waitFor(0)
|
||||||
|
.then((result) async {
|
||||||
|
expect(result != null, true);
|
||||||
|
r3 = await result!;
|
||||||
|
});
|
||||||
|
|
||||||
|
final c = await tracker.waitFor(1);
|
||||||
|
expect(c, null);
|
||||||
|
|
||||||
|
// Resolve jobs
|
||||||
|
await tracker.resolve(0, 42);
|
||||||
|
await tracker.resolve(1, 25);
|
||||||
|
await tracker.resolve(2, -1);
|
||||||
|
|
||||||
|
expect(r2, 42);
|
||||||
|
expect(r3, 42);
|
||||||
|
});
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
import 'package:moxxmpp/moxxmpp.dart';
|
import 'package:moxxmpp/moxxmpp.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../helpers/logging.dart';
|
||||||
import '../helpers/xmpp.dart';
|
import '../helpers/xmpp.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
initLogger();
|
||||||
|
|
||||||
test('Test having multiple disco requests for the same JID', () async {
|
test('Test having multiple disco requests for the same JID', () async {
|
||||||
final fakeSocket = StubTCPSocket(
|
final fakeSocket = StubTCPSocket(
|
||||||
play: [
|
play: [
|
||||||
@ -102,7 +105,7 @@ void main() {
|
|||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
expect(
|
expect(
|
||||||
disco.getRunningInfoQueries(DiscoCacheKey(jid.toString(), null)).length,
|
disco.infoTracker.getRunningTasks(DiscoCacheKey(jid.toString(), null)).length,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
fakeSocket.injectRawXml("<iq type='result' id='${fakeSocket.lastId!}' from='romeo@montague.lit/orchard' to='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>");
|
fakeSocket.injectRawXml("<iq type='result' id='${fakeSocket.lastId!}' from='romeo@montague.lit/orchard' to='polynomdivision@test.server/MU29eEZn' xmlns='jabber:client'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>");
|
||||||
@ -111,6 +114,6 @@ void main() {
|
|||||||
|
|
||||||
expect(fakeSocket.getState(), 6);
|
expect(fakeSocket.getState(), 6);
|
||||||
expect(await result1, await result2);
|
expect(await result1, await result2);
|
||||||
expect(disco.hasInfoQueriesRunning(), false);
|
expect(disco.infoTracker.hasTasksRunning(), false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user