Skip to content

fix(coordinator,sync,plugins): bundled MED bug fixes + dead AppKit import drop#1177

Merged
datlechin merged 1 commit into
mainfrom
fix/med-bugs-and-quick-wins-bundle
May 10, 2026
Merged

fix(coordinator,sync,plugins): bundled MED bug fixes + dead AppKit import drop#1177
datlechin merged 1 commit into
mainfrom
fix/med-bugs-and-quick-wins-bundle

Conversation

@datlechin
Copy link
Copy Markdown
Member

Summary

Five small audit fixes bundled into one PR (per request to reduce per-PR review overhead):

E4 — SchemaProviderRegistry.retain leak on throwaway coordinators

MainContentCoordinator.init unconditionally called services.schemaProviderRegistry.retain(for:). SwiftUI body re-evaluation creates throwaway coordinators that @State discards before markActivated; those instances retained the schema provider but never released it (deinit's never-activated branch did call release, which on its own was an over-release for any future scenario where retain is conditional).

The retain now lives in markActivated (gated by a one-shot flip of _didActivate), pairing with the existing release in teardown / deinit. The never-activated deinit branch no longer calls release for an instance that never retained.

C1 — databaseDidConnect task writes into tearing-down coordinator

MainContentCommandActions.handleDatabaseDidConnect spawned a Task that captured self strongly and called coordinator?.X after each step. After await coordinator?.refreshTables(), the task continued to call coordinator?.initRedisKeyTreeIfNeeded() even if the user had disconnected mid-fetch and the coordinator was tearing down.

Fix: capture [weak coordinator], guard !coordinator.isTearingDown at top, and re-check after the await so the trailing init call is skipped on teardown.

C2 — SyncCoordinator.observeLocalChanges schedules new task before previous unwinds

The sink did syncTask?.cancel(); syncTask = Task { ... }, so the new task started its 2s sleep before the cancelled previous task's try? Task.sleep had returned. Result: brief overlap with two live sync tasks during rapid local changes.

Fix:

  • The new task now awaits previousTask?.value before starting its own sleep, so previous fully unwinds first.
  • syncNow gains its own guard !syncStatus.isSyncing so it short-circuits if a sync is already in progress (the call-site !isSyncing check could race with the actor hop).

P1 — built-in plugins skipped pluginKitVersion check

The if source == .userInstalled guard in PluginManager.validatePluginKitVersion made the version equality check apply only to user-installed plugins. A built-in whose Info.plist TableProPluginKitVersion fell behind would load anyway and crash on the first new protocol-witness method call (per the EXC_BAD_INSTRUCTION note in CLAUDE.md). Now the check applies to all sources; built-ins ship together so this catches developer error before release.

1.3a — drop unused import AppKit from Models/Settings/AppSettings.swift

Pure data structures; no AppKit usage. The other Models the audit flagged either really do use AppKit (ColumnIdentitySchema references NSUserInterfaceItemIdentifier) or rely on Color(nsColor:) which would require visual-equivalent replacements (ConnectionToolbarState); both are out of scope for a clean drop.

Skipped from this bundle (with reasons)

  • 3.5 (static switchSeq counter): the global-counter property is intentional for cross-coordinator log correlation; per-instance + instance-id is a stylistic choice not a correctness fix.
  • 5.4 (IUO outlets in MainSplitViewController): 46 access sites for what audit flagged LOW; net noise.
  • P3 (PluginQueryResult.expectedRowCount): adds a new field to a struct that crosses the plugin/host transport boundary; needs every distributed plugin rebuilt with the new ABI.
  • E3 (insert-then-delete same row): test-only verification, not a code fix.

Test plan

  • Open the app, create and immediately discard a transient SwiftUI view that constructs a MainContentCoordinator (any tab switch should do); confirm SchemaProviderRegistry.purgeUnused does not log under-released errors.
  • Connect to a slow remote PG; mid-refreshTables, close the window. Confirm the trailing initRedisKeyTreeIfNeeded log line does not appear.
  • Make rapid local changes (rename a connection, edit favorites) so the syncChangeTracked debounce fires repeatedly. Confirm no two-sync-tasks-live state.
  • Sideload a built-in plugin with a stale TableProPluginKitVersion; confirm it now refuses to load with pluginOutdated rather than crashing.
  • swiftlint --strict clean.

@datlechin datlechin merged commit 3ed9a6c into main May 10, 2026
2 checks passed
@datlechin datlechin deleted the fix/med-bugs-and-quick-wins-bundle branch May 10, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant