addRequest<T> method

Future<T?> addRequest<T>(
  1. Disposable? isAlive,
  2. Future<T?> request()
)

Public so that other related classes such as InspectorService can ensure their requests are in a consistent order with existing requests.

When oneRequestAtATime is true, using this method eliminates otherwise surprising timing bugs, such as if a request to dispose an InspectorService.ObjectGroup was issued after a request to read properties from an object in a group, but the request to dispose the object group occurred first.

With this design, we have at most 1 pending request at a time. This sacrifices some throughput, but we gain the advantage of predictable semantics and the ability to skip large numbers of requests from object groups that should no longer be kept alive.

The optional ObjectGroup specified by isAlive indicates whether the request is still relevant or should be cancelled. This is an optimization for the Inspector so that it does not overload the service with stale requests. Stale requests will be generated if the user is quickly navigating through the UI to view specific details subtrees.

Implementation

Future<T?> addRequest<T>(
  Disposable? isAlive,
  Future<T?> Function() request,
) async {
  if (isAlive != null && isAlive.disposed) return null;

  if (!oneRequestAtATime) {
    return request();
  }
  // Future that completes when the request has finished.
  final response = Completer<T?>();
  // This is an optimization to avoid sending stale requests across the wire.
  void wrappedRequest() async {
    if (isAlive != null && isAlive.disposed || _disposed) {
      response.complete(null);
      return;
    }
    try {
      final value = await request();
      if (!_disposed && value is! Sentinel) {
        response.complete(value);
      } else {
        response.complete(null);
      }
    } catch (e) {
      if (_disposed || isAlive?.disposed == true) {
        response.complete(null);
      } else {
        response.completeError(e);
      }
    }
  }

  if (allPendingRequestsDone == null || allPendingRequestsDone!.isCompleted) {
    allPendingRequestsDone = response;
    wrappedRequest();
  } else {
    if (isAlive != null && isAlive.disposed || _disposed) {
      response.complete(null);
      return response.future;
    }

    final previousDone = allPendingRequestsDone!.future;
    allPendingRequestsDone = response;
    // Schedule this request only after the previous request completes.
    try {
      await previousDone;
    } catch (e, st) {
      if (!_disposed) {
        _log.shout(e, e, st);
      }
    }
    wrappedRequest();
  }
  return response.future;
}