Skip to content

refactor(lsp): effectify LSP service with InstanceState#19150

Merged
kitlangton merged 15 commits intodevfrom
kit/effectify-lsp
Mar 26, 2026
Merged

refactor(lsp): effectify LSP service with InstanceState#19150
kitlangton merged 15 commits intodevfrom
kit/effectify-lsp

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

Summary

Migrate LSP from Instance.state() to Effect Service/Layer/InstanceState pattern with makeRunPromise and async facades.

  • InstanceState for per-directory lifecycle with ScopedCache
  • Effect.fn tracing on all 14 service methods
  • Effect.fnUntraced for internal helpers (getClients, runForFile, runForAll, callHierarchyRequest)
  • addFinalizer for client shutdown on instance disposal
  • Extract callHierarchyRequest helper (incoming/outgoing calls were near-identical)
  • No changes to server.ts, client.ts, launch.ts, or language.ts
  • No consumer changes — all 15+ callers use unchanged async facades

Test plan

  • 18 LSP tests pass (6 existing + 12 new lifecycle tests)
  • Zero typecheck errors
  • Correctness audit: line-by-line comparison confirmed identical behavior
  • Verify LSP servers connect and diagnostics appear in a live session

Migrate LSP from Instance.state() to Effect Service/Layer/InstanceState
pattern with makeRunPromise and async facades.

- InstanceState for per-directory lifecycle with ScopedCache
- Effect.fn tracing on all 14 service methods
- Effect.fnUntraced for internal helpers (getClients, runForFile, runForAll)
- addFinalizer for client shutdown on instance disposal
- No changes to server.ts, client.ts, launch.ts, or language.ts
- No consumer changes — all 15+ callers use unchanged async facades

const getClients = Effect.fnUntraced(function* (file: string) {
if (!Instance.containsPath(file)) return [] as LSPClient.Info[]
const s = yield* InstanceState.get(cache)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually call this state (not cache)

@thdxr thdxr marked this pull request as ready for review March 25, 2026 19:39
Same pattern as the message/part projectors — catch and log FK
constraint errors instead of throwing. Fixes flaky CI where test
ordering causes Session.create to fire before the project row exists.
@kitlangton
Copy link
Copy Markdown
Contributor Author

/review

log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style suggestion: The old code used run and runAll as helper names. Consider keeping these shorter names instead of runForFile and runForAll. Per style guide: "Use single word names by default for new locals, params, and helper functions."

expect(Array.isArray(result)).toBe(true)
expect(result.length).toBe(0)
spawnSpy.mockRestore()
}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Consider moving spawnSpy.mockRestore() to an afterEach hook instead of repeating it in each test. The beforeEach already handles setup, and cleanup in afterEach would reduce repetition and ensure cleanup even if tests fail.

@github-actions
Copy link
Copy Markdown
Contributor

Code review complete. Overall the Effect migration looks solid and follows the documented patterns well. I left two minor style suggestions on the code:

  1. lsp/index.ts: Consider keeping the shorter helper names run/runAll (from the original code) instead of the longer runForFile/runForAll
  2. lifecycle.test.ts: The repeated spawnSpy.mockRestore() calls could move to an afterEach hook

These are just suggestions - the code is otherwise clean and the migration approach is correct. The try/catch in projectors.ts is appropriate for handling the foreign key constraint error.

Comment on lines +66 to +71
try {
db.insert(SessionTable).values(Session.toRow(data.info)).run()
} catch (err) {
if (!foreign(err)) throw err
log.warn("ignored session create — project missing", { sessionID: data.info.id, projectID: data.info.projectID })
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. This probably should not be part of this PR.

- Rename `cache` → `state` for InstanceState (convention)
- Rename `runForFile`/`runForAll` → `run`/`runAll` (shorter helpers)
- Move `spawnSpy.mockRestore()` to `afterEach` in lifecycle tests
- Revert unrelated projectors.ts try/catch change
@kitlangton kitlangton merged commit c7d2309 into dev Mar 26, 2026
8 checks passed
@kitlangton kitlangton deleted the kit/effectify-lsp branch March 26, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant