# Language Server Architecture

> Apex Language Server data architecture — type sources, worker topology, and data ownership. Use when working on type resolution, worker communication, resource loading, hover, definition, diagnostics, or any code in apex-ls/src/server/ or apex-parser-ast/src/symbols/.

- Skill: `forcedotcom/language-server-architecture` (Agent Skill)
- Install (CLI): `npx skillmds@latest add forcedotcom/language-server-architecture`
- Raw SKILL.md: https://api.skillmd.com/api/skills/forcedotcom/language-server-architecture/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: forcedotcom (https://skillmd.com/u/forcedotcom)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/forcedotcom/language-server-architecture

---


# Language Server Architecture

## Hard Rule: No Raw-Text Semantic Fallbacks

Language features must never scan raw Apex source with regular expressions or
other text heuristics to infer symbols, declarations, types, references,
scopes, members, completion candidates, definitions, or dependency and
missing-artifact names. Semantic results must come from the Apex lexer/parser,
grammar-derived tree walkers, symbol tables, resolved references/xref data, or
a prior stable semantic snapshot.

Raw text may be passed to the parser and used for document transport or
position/range mechanics only. When semantic state is incomplete or stale,
wait, retry, or return an incomplete result; do not invent semantic state from
source text.

## Three Sources of Apex Types

The language server works with three distinct sources of Apex type artifacts:

### 1. Workspace (customer code)
- `.cls`, `.trigger`, `.apex` files authored by the user
- Managed by the **data owner worker** via didOpen/didChange/didSave
- Compiled on-demand by the parser; symbols registered in the symbol graph

### 2. Faux SObject classes
- Files representing fields found in SObjects (Account, Contact, Property__c, etc.)
- Produced by an **external process outside this repository** at the user's request
- Placed at a well-known path in the workspace (`.sfdx/tools/`)
- Presented to the language server using the same document operations as customer code
- Their URI comes from a path the language server controls
- **NOT part of the standard Apex library** — do not confuse with stdlib

### 3. Standard Apex Library (stdlib)
- System.String, System.Assert, Database.QueryLocator, ConnectApi.*, etc.
- Owned exclusively by the **resource loader worker**
- Backed by three read-only precompiled artifacts in `apex-parser-ast/resources/`:
  - `apex-type-registry.pb.gz` — protobuf metadata about stdlib symbols (6112 types, 59 namespaces)
  - `apex-stdlib.pb.gz` — precompiled symbol data
  - `StandardApexLibrary.zip` — source `.cls` files for stdlib classes
- All access to these artifacts MUST go through the resource loader worker

## Worker Topology

```
Coordinator (main thread)
├── Data owner worker      — owns symbol tables for workspace + faux SObject files
├── Enrichment workers (2+) — handle hover, definition, diagnostics
└── Resource loader worker — owns standard Apex library (read-only)
```

### Data owner worker
- Single worker, source of truth for customer code and faux SObject class symbols
- Receives document lifecycle events (open, change, save, close)
- Serves symbol data to enrichment workers via QuerySymbolSubset
- Accepts write-backs of enriched symbols from enrichment workers

### Enrichment workers
- Pool of workers for parallel LSP request processing
- Get file symbols from data owner, process requests, write back enriched data
- Access stdlib via remote ResourceLoaderService layer (assistance channel → coordinator → resource loader worker)
- Do NOT have local copies of protobuf caches, stdlib ZIP, or GlobalTypeRegistry
- The sync namespace cache (populated during stdlib warmup) is the only local stdlib state

### Resource loader worker
- Single worker, owns all standard Apex library data
- Loads and serves stdlib symbol tables, resolves class FQNs, provides namespace index
- All stdlib access from other workers routes through this worker

## GlobalTypeRegistry

Defined in `apex-parser-ast/src/services/GlobalTypeRegistryService.ts`.

GlobalTypeRegistry is an **in-process, mutable type index** — NOT just a stdlib cache. It tracks types from ALL three sources as they are compiled and registered:

- Stdlib types registered at startup via `ResourceLoader.initializeTypeRegistry()` (`isStdlib: true`)
- User types registered via `ApexSymbolManager.addSymbolTable()` (`isStdlib: false`)
- Provides O(1) cross-file type resolution via `resolveType()`

It is a **module-level singleton** (`globalRegistryInstance`). Each Node.js process gets its own instance:
- **Coordinator process**: populated with stdlib + user types as files are compiled
- **Enrichment workers**: EMPTY — enrichment workers don't compile files or load protobuf caches

When modifying resolution code in `apex-parser-ast/src/symbols/ops/`:
- Code runs on BOTH coordinator and enrichment workers
- Do not remove GlobalTypeRegistry usage — it provides cross-file resolution on the coordinator
- Ensure enrichment worker fallback paths work correctly (they bypass the empty registry)

## Separation of Concerns (Data Ownership Boundaries)

Keep data-handling logic siloed within the component whose responsibility it is. Each layer of the topology has one job:

- **Coordinator** — routing and message passing only. It dispatches requests to the right worker and relays responses. It does NOT transform, enrich, parse, or reshape symbol data; that belongs to the worker that owns the data.
- **Data owner worker** — owns the symbol graph for workspace + faux SObject files. All mutation, merging, and write-back reconciliation of those symbols lives here.
- **Request/enrichment workers** — process LSP requests against symbols they fetch from the data owner; transformation of request results lives here, not in the coordinator.
- **Resource loader worker** — owns all stdlib access and any reshaping of stdlib artifacts.

Rule of thumb: if you're adding data transformation logic to the coordinator, it's in the wrong place — push it down to the owning worker. A component should only manipulate data it owns.

## LSP Request Routing (DISPATCH_ROUTING)

Defined in `apex-ls/src/server/WorkerCoordinator.ts`.

| Request | Target | Notes |
|---------|--------|-------|
| documentOpen/Change/Save/Close | dataOwner | Document lifecycle |
| hover | enrichmentPool | With write-back |
| definition | enrichmentPool | With write-back |
| diagnostics | enrichmentPool | With write-back |
| completion, references, etc. | coordinatorOnly | Not yet moved to workers |

## Worker Profiling & Debugging

Workers inherit profiling, debug, and heap-size flags from the main process via `WorkerExecArgvBuilder`. Key behaviors:

- **Profiling** (`apex.environment.profilingMode: "full"`): Workers get `--cpu-prof` / `--heap-prof` with role-specific subdirectories (e.g. `<output>/dataOwner/`)
- **Debug** (`apex.debug: "inspect"`): Workers get `--inspect=0` (auto-assigned port). The main process keeps its fixed port (default 6009).
- **Heap size**: `--max-old-space-size` passes through unchanged.
- **Thread naming**: Workers are named `apex-worker-<role>` (visible in Chrome DevTools).
- **Stderr forwarding**: Worker stderr is line-buffered and logged with role labels. Debug port assignments appear in the Output panel.

## Worker Log Reachability

From inside a worker, only two log paths reach the coordinator's output channel:

1. **Effect logs** (`Effect.logInfo`/`logDebug`/etc.) executed under the worker's replaced `workerLogger` layer — these post a `WorkerLogMessage` over the assistance port.
2. **`emitWorkerLog(level, msg)`** (in `worker.platform.ts`) — posts a `WorkerLogMessage` directly; works from ANY context.

These do NOT reach the channel:

- **`getLogger().info/debug/warn()`** from an async request handler (e.g. `DispatchReferences`) — writes to a logger not wired to the parent port.
- **`Effect.logInfo` run under an ad-hoc `Effect.runPromise`** inside a handler — uses the default logger, not `workerLogger`.

**Rule of thumb:** for diagnostics in an async worker handler, use `emitWorkerLog`. If you must log from deep inside an Effect that a handler runs via `Effect.runPromise` (e.g. `addSymbolTable`), stash the message on an instance field and have the handler emit it via `emitWorkerLog` after the run.

## Key Paths

- Worker entry: `packages/apex-ls/src/worker.platform.ts`
- Worker coordinator: `packages/apex-ls/src/server/WorkerCoordinator.ts`
- Worker execArgv builder: `packages/apex-ls/src/server/WorkerExecArgvBuilder.ts`
- Assistance mediator: `packages/apex-ls/src/server/CoordinatorAssistanceMediator.ts`
- Symbol resolution ops: `packages/apex-parser-ast/src/symbols/ops/`
- GlobalTypeRegistry: `packages/apex-parser-ast/src/services/GlobalTypeRegistryService.ts`
- Resource loader: `packages/apex-parser-ast/src/utils/resourceLoader.ts`
- Enrichment bootstrap: `packages/lsp-compliant-services/src/workers/WorkerBackendBootstrap.ts`

