interact method

Runs one interaction against exactly the observed session/revision. Retry an uncertain response by observing; never replay a new request ID.

Implementation

Future<MiniProgramInteractionResult> interact(
  MiniProgramInteractionRequest request,
) async {
  if (_executing) return _result(request, 'rejected', 'interaction_busy');
  _executing = true;
  try {
    if (!await _frame()) {
      return _result(
        request,
        'rejected',
        'render_pending',
        renderPending: true,
      );
    }
    final snapshot = _observeNow(sessionId: request.sessionId);
    final surface = _surfaces[request.sessionId];
    if (_disposed || surface == null || !surface.current) {
      return _result(request, 'rejected', 'foreground_required');
    }
    if (_seen.containsKey(request.requestId)) {
      return _result(request, 'rejected', 'duplicate_request');
    }
    if (snapshot.revision != request.revision) {
      return _result(request, 'rejected', 'stale_view');
    }
    if (surface.pending) {
      return _result(request, 'pending', 'interaction_pending');
    }
    final target = surface.targets[request.targetId];
    if (request.action != MiniProgramInteractionAction.back &&
        (target == null || !target.actions.contains(request.action))) {
      return _result(request, 'rejected', 'target_unavailable');
    }
    // Bound replay memory. Revision consumption also prevents old commands
    // from becoming executable again after an entry is evicted.
    _seen[request.requestId] = request.sessionId;
    if (_seen.length > 256) _seen.remove(_seen.keys.first);
    surface.generation++;
    final userGeneration = surface.userGeneration;
    final execution = MiniProgramInteractionExecution(
      isAllowed: () =>
          !_disposed &&
          surface.current &&
          surface.userGeneration == userGeneration,
    );
    surface.executions.add(execution);
    try {
      if (request.action == MiniProgramInteractionAction.back) {
        if (!await execution.run(() => _back(surface))) {
          return _result(request, 'rejected', 'back_unavailable');
        }
      } else if (request.action == MiniProgramInteractionAction.enterText) {
        if (!target!.node.getSemanticsData().hasAction(
          SemanticsAction.setText,
        )) {
          execution.run(
            () => target.owner.performAction(
              target.node.id,
              SemanticsAction.tap,
            ),
          );
          await _frame();
        }
        // Focus/keyboard changes may alter the tree. Check this exact live
        // node and ownership again before writing any text.
        _capture(surface);
        final focused = surface.targets[request.targetId];
        if (!execution.isAllowed() ||
            focused == null ||
            !identical(focused.node, target.node) ||
            !focused.node.getSemanticsData().hasAction(
              SemanticsAction.setText,
            )) {
          return _result(request, 'rejected', 'target_unavailable');
        }
        execution.run(
          () => focused.owner.performAction(
            focused.node.id,
            SemanticsAction.setText,
            request.text,
          ),
        );
      } else {
        execution.run(
          () => target!.owner.performAction(
            target.node.id,
            _semanticActions[request.action]!,
          ),
        );
      }
      var rendered = await _frame();
      rendered = await _settleRoutes(surface) && rendered;
      // A second frame catches local state and route changes without waiting
      // indefinitely for animations, open dialogs, or a backend response.
      rendered = await _frame() && rendered;
      final last = execution.lastResult;
      final code = last is HostActionResult && !last.isSuccess
          ? last.errorCode ?? 'action_failed'
          : last is MiniProgramBackendResult && !last.isSuccess
          ? last.errorCode ?? 'backend_failed'
          : null;
      final status = execution.cancelled || !execution.isAllowed()
          ? 'interrupted'
          : execution.failed || code != null
          ? 'failed'
          : surface.pending || !rendered
          ? 'pending'
          : 'completed';
      return _result(request, status, code, renderPending: !rendered);
    } on Object {
      return _result(request, 'failed', 'interaction_failed');
    } finally {
      execution.active = false;
    }
  } finally {
    _executing = false;
  }
}