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;
}
}