reattach method

Future<ReattachResult> reattach({
  1. required Client client,
  2. required MCPUIRuntime runtime,
  3. required String ownerKey,
})

Re-issues every subscription recorded for ownerKey on a NEW client.

A subscription belongs to the CONNECTION. After a background round trip the connection is torn down and rebuilt, and the server has no memory of what the previous link had subscribed — the stream simply stops. The runtime bindings, on the other hand, live in the runtime and survive, so nothing on screen looks wrong and pressing Subscribe again is a no-op from the runtime's point of view. Only the wire call has to be redone.

Bindings are therefore NOT re-registered here; the initial read is, so the first value after a resume is current rather than whatever was on screen when the app went away.

Implementation

Future<ReattachResult> reattach({
  required Client client,
  required MCPUIRuntime runtime,
  required String ownerKey,
}) async {
  final uris = _active[ownerKey];
  if (uris == null || uris.isEmpty) return const ReattachResult();
  var resubscribed = 0;
  var failed = 0;
  for (final uri in List<String>.from(uris)) {
    try {
      await SharedResourceSubscriptions.subscribe(client, uri);
      resubscribed++;
    } catch (e, st) {
      failed++;
      _logger.logError('resubscribe after reconnect failed', e, st,
          {'uri': uri, 'ownerKey': ownerKey});
      continue;
    }
    try {
      final resource = await client.readResource(uri);
      if (resource.contents.isEmpty) continue;
      final text = resource.contents.first.text;
      if (text == null) continue;
      final decoded = jsonDecode(text);
      if (decoded is Map<String, dynamic>) {
        decoded.forEach(runtime.stateManager.set);
      }
    } catch (e) {
      _logger.warn('resubscribe initial read failed', {'uri': uri}, e);
    }
  }
  // Successes and failures, separately. This used to report the number of
  // URIs ATTEMPTED under the name `count`, so a reattach where every
  // resource was refused still printed `resubscribed … count: 2` with the
  // failures on their own earlier lines — which reads as success and was
  // misread as success by someone debugging a device that had gone quiet.
  _logger.info('resubscribed after reconnect', {
    'ownerKey': ownerKey,
    'resubscribed': resubscribed,
    'failed': failed,
  });
  return ReattachResult(resubscribed: resubscribed, failed: failed);
}