Files
agame/addons/godot_ai/utils/mcp_client_refresh_state.gd
2026-09-21 16:16:59 +08:00

102 lines
3.9 KiB
GDScript

@tool
class_name McpClientRefreshState
extends RefCounted
## State machine for the plugin-lifetime client's status refresh sweep. Single
## source of truth — supersedes the seven booleans + deadline previously
## scattered across `mcp_dock.gd` (`_client_status_refresh_in_flight`,
## `_client_status_refresh_pending`, `_client_status_refresh_pending_force`,
## `_client_status_refresh_timed_out`, `_client_status_refresh_started_msec`,
## `_client_status_refresh_deferred_until_filesystem_ready`,
## `_client_status_refresh_deferred_force`,
## `_client_status_refresh_deferred_initial`,
## `_client_status_refresh_shutdown_requested`).
##
## The ints are stable for tests; reordering is a breaking change.
## No worker running, no pending request. Default state.
const IDLE := 0
## A refresh request landed but the editor filesystem is busy
## (`EditorInterface.get_resource_filesystem().is_scanning()` is true);
## the job owner parks the request and retries on the next `_process` after
## the scan settles. A separate force bit preserves an explicit retry request.
const DEFERRED_FOR_FILESYSTEM := 1
## Worker thread is alive and probing client status off-main. The
## Dock snapshots paint "(checking...)" in the clients summary and the owner accepts
## additional requests as `pending`.
const RUNNING := 2
## Worker has been alive past CLIENT_STATUS_REFRESH_TIMEOUT_MSEC. The
## dock paints "(client probe still running)"; further requests coalesce into
## one retry after this sole worker exits.
const RUNNING_TIMED_OUT := 3
## Plugin teardown / update activation is draining workers. New refresh
## requests are rejected outright until an explicitly failed handoff resumes
## the same owner.
const SHUTTING_DOWN := 4
const _NAMES := {
IDLE: "idle",
DEFERRED_FOR_FILESYSTEM: "deferred_for_filesystem",
RUNNING: "running",
RUNNING_TIMED_OUT: "running_timed_out",
SHUTTING_DOWN: "shutting_down",
}
static func name_of(state: int) -> String:
return _NAMES.get(state, "unknown(%d)" % state)
## True when a worker thread should be alive in this state. Combined
## state — RUNNING or RUNNING_TIMED_OUT both have the sole worker running.
static func has_worker_alive(state: int) -> bool:
return state == RUNNING or state == RUNNING_TIMED_OUT
## True while the status worker is still within its healthy budget. Once a
## refresh has timed out, the dock keeps the warning badge but must let users
## retry Configure / Configure all instead of stranding the controls behind an
## orphaned, uninterruptible worker.
static func should_disable_client_actions(state: int) -> bool:
return state == RUNNING
## True when the dock should reject new refresh spawns. Used by the
## job owner's deferred entrypoint and status-refresh scheduler.
static func is_blocked_for_spawn(state: int) -> bool:
return state == SHUTTING_DOWN
## True when the summary label should show the in-flight badge.
static func should_show_checking_badge(state: int) -> bool:
return state == RUNNING or state == RUNNING_TIMED_OUT
## Transition table. Same shape as McpServerState — illegal transitions
## return false; callers `push_warning` and no-op.
static func can_transition(from: int, to: int) -> bool:
if from == to:
return true
## Shutdown is sticky.
if from == SHUTTING_DOWN:
return false
## Anything → SHUTTING_DOWN is legal (drain on _exit_tree / install).
if to == SHUTTING_DOWN:
return true
match from:
IDLE:
return to == RUNNING or to == DEFERRED_FOR_FILESYSTEM
DEFERRED_FOR_FILESYSTEM:
## When the filesystem scan settles we either spawn a worker
## (RUNNING) or roll back to IDLE if no rows need probing.
return to == RUNNING or to == IDLE
RUNNING:
## Worker finishes -> IDLE. Worker outlives budget ->
## RUNNING_TIMED_OUT.
return to == IDLE or to == RUNNING_TIMED_OUT
RUNNING_TIMED_OUT:
## The sole late-arriving worker drops back to IDLE. A coalesced
## retry then starts through the ordinary IDLE -> RUNNING edge.
return to == IDLE
return false