move daemon services to backend isolate

The merged UI/platform thread on macOS (Flutter 3.41) freezes on any
synchronous work — file I/O, Process.run, even isolate spawning during
early frames. No timing workaround (Timer, addPostFrameCallback,
Future.delayed) was reliable.

Fix: spawn a backend isolate that owns the DaemonDispatcher, GitClient,
PqlClient, FilesService, EditorRegistry, and Toolchain resolution. The
main isolate stays free for rendering. Communication uses SendPort with
the existing IPC message protocol (IpcRequest/IpcResponse/IpcEvent) —
zero new serialization.

New files:
  backend.dart       — spawns isolate, manages SendPort/ReceivePort
  backend_entry.dart — isolate entry point, boots all services
  isolate_client.dart — replaces InProcessClient for production

Toolchain now exposes resolveToolchainPaths() as a top-level function
with self-contained PATH expansion (no module-level state that would
prevent isolate message passing).

ClideTestApp gains boot-sequence tests: compute(), Isolate.run(),
sequential Process.run, and the full resolve+exec chain.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-04-26 13:53:33 +02:00
co-authored by Claude Opus 4.6
parent 9fff157c9e
commit a89910dc36
9 changed files with 543 additions and 171 deletions
+91
View File
@@ -0,0 +1,91 @@
/// Manages the backend isolate lifecycle.
///
/// Call [spawn] to start the backend, which resolves the toolchain and
/// boots all daemon services in a separate isolate. Returns when the
/// backend is ready to accept requests.
library;
import 'dart:async';
import 'dart:isolate';
import 'package:clide/kernel/src/backend_entry.dart';
import 'package:clide/kernel/src/ipc/isolate_client.dart';
import 'package:clide/kernel/src/toolchain.dart';
class Backend {
Backend._({
required this.client,
required this.toolchain,
required Isolate isolate,
required ReceivePort receivePort,
}) : _isolate = isolate,
_receivePort = receivePort;
final IsolateClient client;
final Toolchain toolchain;
final Isolate _isolate;
final ReceivePort _receivePort;
/// Spawn the backend isolate and wait for it to be ready.
static Future<Backend> spawn({
required String workspaceRoot,
required IsolateClient Function(SendPort backendPort) clientFactory,
}) async {
final receivePort = ReceivePort();
final completer = Completer<Backend>();
late final IsolateClient client;
late final Isolate isolate;
receivePort.listen((message) {
if (message is Map<String, Object?>) {
final type = message['type'] as String?;
if (type == 'ready') {
// Backend is booted — extract its request port and toolchain state.
final requestPort = message['requestPort'] as SendPort;
client = clientFactory(requestPort);
// Apply toolchain state from the backend.
final tcData = message['toolchain'] as Map<String, Object?>;
final toolchain = Toolchain();
toolchain.applyResolved(ResolvedPaths(
git: tcData['git'] as String?,
pql: tcData['pql'] as String?,
tmux: tcData['tmux'] as String?,
ptyc: tcData['ptyc'] as String?,
shell: tcData['shell'] as String?,
gitEnv: (tcData['gitEnv'] as Map?)?.cast<String, String>(),
));
if (!completer.isCompleted) {
completer.complete(Backend._(
client: client,
toolchain: toolchain,
isolate: isolate,
receivePort: receivePort,
));
}
} else {
// Response or event — forward to the client.
client.handleMessage(message);
}
}
});
isolate = await Isolate.spawn(
backendEntry,
BackendBootMessage(
frontendPort: receivePort.sendPort,
workspaceRoot: workspaceRoot,
),
);
return completer.future;
}
/// Shut down the backend isolate.
void dispose() {
_isolate.kill(priority: Isolate.beforeNextEvent);
_receivePort.close();
}
}