WSO2 MI Language Server
/ Flows & Sequences
Deep Dive

Flows & Sequences

How things actually happen at runtime — the six flows that cover almost everything the server does.

1 · Startup & initialization

Simple two-step handshake. The extension scans the workspace for pom.xml files, detects the MI version, and sends the matching schema nickname ("430" or "440") to the server at startup via initializationOptions. The server looks up the bundled XSD for that nickname and registers it. Validation is deferred until initialized is received.

sequenceDiagram autonumber participant C as VS Code Client participant S as server.ts participant SA as SchemaAssociator C->>S: initialize(params) Note over S: read initializationOptions.schemas
{pattern, schema: "440"} S->>SA: registerSchemas() — pattern → bundled 430/440 S-->>C: capabilities (completion, hover, …) C->>S: initialized Note over S: initialConfigurationLoaded = true S->>S: validateOpenDocumentsSafely()

How schemas are chosen: The extension reads each workspace folder's pom.xml, extracts <project.runtime.version>, and maps it to a schema folder — 430 for MI 4.3 and below, 440 for 4.4 and above. No manual configuration needed.

Why the deferral? If a document change arrived before initialized, it would try to validate before schemas were registered. The initialConfigurationLoaded flag gates all validation until the handshake completes.

2 · Document change → diagnostics

The main loop. Edits are debounced 300 ms per document, so a typing burst runs this once after the user pauses. The JS AST (parseXMLDocument) is built lazily — skipped entirely in the common case where a filename pattern resolves the schema directly. The WASM validator runs on every edit but its instance is shared across all files using the same schema. A version guard at publish time discards results that went stale while validation was running.

sequenceDiagram autonumber participant C as Client participant S as server.ts participant DH as DiagnosticsHandler participant SP as SchemaProvider participant W as Xerces WASM C->>S: textDocument/didChangeContent Note over S: debounce 300 ms per URI
burst of keystrokes → one run S->>DH: validateAndSend(document) DH->>DH: capture document.version Note over DH,SP: pass 1 — resolve by filename pattern only (no parse) DH->>SP: resolveSchemaForDocument(file, xmlns=undefined, path) alt no pattern matched Note over DH: lazy parse — only if pass 1 missed DH->>DH: parseXMLDocument() — extract xmlns DH->>SP: resolveSchemaForDocument(file, xmlns, path) end alt schema resolved opt first time for THIS file (!hasSchema) Note over DH,SP: engine 2 — register completion schema (once per file) DH->>DH: loadReferencedXsds() — follow XSD imports DH->>SP: registerSchema(auto://path, imports) end Note over DH,W: engine 1 — WASM validation (every edit, uses raw text not AST) DH->>DH: getOrCreateValidator(xsdPath) Note over DH: first time → buildFilesMap() + compile WASM
seen before → reuse cached validator alt ProjectValidator succeeds DH->>W: validator.validate(sanitizedText) W-->>DH: parseErrors + schemaErrors DH->>DH: deduplicate + map ranges else ProjectValidator fails (or no absolute xsdPath) DH->>SP: service.validate(autoUri, doc) Note over SP: fallback to @xml-tools/parser
syntax errors if no validator SP-->>DH: raw diagnostics DH->>DH: deduplicate + map ranges end alt version unchanged DH-->>C: publishDiagnostics(uri, [...]) else newer edits arrived meanwhile Note over DH: stale — result discarded end else no schema matched DH-->>C: publishDiagnostics(uri, []) — clear stale end

Why lazy parse? Building the JS AST on every keystroke was expensive — and wasted, because the WASM engine takes raw text and re-parses internally in C++. In the common case (file inside a pom.xml-detected project), the schema is resolved by filename pattern alone, so parseXMLDocument is never called during validation. It is only triggered as a fallback when no pattern matches, to extract the XML namespace and retry. It is also triggered if WASM throws and the JS fallback validator is needed.

What gets reused vs. created fresh

ThingScopeWhen createdWhen reused
JS AST (parseXMLDocument)Per validation runOnly if pattern match failed or WASM threwCached within the same run via getXmlDoc()
Completion schema (auto://)Per fileFirst edit of that fileAll later edits of the same file
WASM validator (ProjectValidator)Per XSD folderFirst file using that schemaAll other files using the same schema

Example: two projects, three files

  1. Open project1/proxy.xml Pattern matches → 440 XSD. No parse needed. Completion schema registered for proxy.xml. WASM validator for 440 created and cached.
  2. Open project2/sequence.xml Pattern matches → 430 XSD. No parse needed. Completion schema registered for sequence.xml. WASM validator for 430 created and cached.
  3. Open project1/api.xml Pattern matches → 440 XSD. No parse needed. Completion schema registered for api.xml. WASM validator for 440 — already cached, reused directly.

Step by step

  1. Debounce Each edit cancels the previous timer; validation fires 300 ms after the last keystroke.
  2. Snapshot version validateAndSend captures document.version on entry — the live document keeps advancing while async work runs.
  3. Resolve pass 1 SchemaAssociator tries to match the file path against registered patterns — no parsing needed. Succeeds in the common case.
  4. Resolve pass 2 (fallback) If pass 1 returned null, the document is parsed to extract its XML namespace, then resolution is retried. Handles files outside any project folder that carry a known namespace.
  5. Register completion schema (once per file) The XSD import graph is loaded and registered under auto://<path>. Skipped on later edits via hasSchema().
  6. Get validator (once per XSD folder) A ProjectValidator is created with every .xsd file compiled into WASM — or the cached one is returned immediately.
  7. Validate Xerces runs syntax + schema validation on the raw text in one SAX pass. If this fails (or no absolute XSD path is available), it falls back to service.validate() to extract syntax/lexical errors from the npm package (@xml-tools/parser).
  8. Deduplicate & map Structural errors appear in both parse and schema error lists — duplicates removed, then mapped to LSP ranges.
  9. Version guard If document.version moved on during validation, the result is discarded.
  10. Publish Diagnostics are sent; if no schema matched, any stale diagnostics are cleared.

Why the version guard? Validations for versions 5 and 6 can finish out of order — if v5's (slower) run completed after v6's, it used to overwrite the correct diagnostics with squiggles computed from text that no longer exists. The guard makes publish order irrelevant; the debounce makes the race rare in the first place.

WASM validator lifecycle & cache

Every call to getOrCreateValidator() either compiles a fresh grammar pool or returns the cached one in a single map lookup. The state diagram shows what happens inside that function; the sequence diagram shows the two runtime paths.

stateDiagram-v2 [*] --> NotCreated : server start / config change NotCreated --> Loading : first XML file opens for this XSD version state Loading { [*] --> ReadDisk ReadDisk --> InRAM : buildFilesMap() · 82 files · ~836 KB in JS heap InRAM --> SentToWASM : createProjectValidator(filesMap) SentToWASM --> [*] : Xerces grammar pool compiled · JS strings GC-eligible } Loading --> Cached : validator ready Cached --> Cached : cache hit · validate() only · no disk read · no WASM recompile Cached --> NotCreated : dispose() · onDidChangeConfiguration
sequenceDiagram autonumber participant F as XML file (edit) participant DH as DiagnosticsHandler participant FS as File System participant W as Xerces WASM rect rgba(20, 50, 20, 0.5) Note over F,W: Cold start — first file for this XSD version F->>DH: validateAndSend(text) DH->>DH: getOrCreateValidator() — cache miss DH->>FS: buildFilesMap(schemaFolder) FS-->>DH: 82 XSD files · ~836 KB loaded into JS heap DH->>W: createProjectValidator(filesMap) Note over W: Xerces compiles grammar pool
into WASM linear memory W-->>DH: ProjectValidator handle Note over DH: filesMap goes out of scope → GC-eligible DH->>W: validator.validate(xmlText) W-->>DH: parseErrors + schemaErrors DH-->>F: publishDiagnostics end rect rgba(20, 20, 55, 0.5) Note over F,W: Warm path — same version, any later edit F->>DH: validateAndSend(text) DH->>DH: getOrCreateValidator() — cache HIT ✓ Note over DH: no disk read · no RAM load · no WASM recompile DH->>W: validator.validate(xmlText) W-->>DH: parseErrors + schemaErrors DH-->>F: publishDiagnostics end

Two versions, two pools: 430 and 440 each get their own ProjectValidator, keyed by the full path to their synapse_config.xsd. Both grammar pools coexist in WASM linear memory simultaneously — 430 files always hit the 430 pool, 440 files always hit the 440 pool, with no cross-contamination.

What the logs look like: Cold start prints three lines — Loaded N XSD files into RAM, XSD text released from RAM, Created ProjectValidator. Every warm-path run prints only Cache hit — reusing ProjectValidator (no XSD reload).

3 · Schema resolution priority

The decision SchemaAssociator.findSchema() makes. User patterns always win over built-in namespace matches.

flowchart TD A["findSchema(file, xmlns, path)"] --> B{user association
pattern matches?} B -- yes --> C["read XSD file"] C --> D{readable?} D -- yes --> E["✅ return custom schema"] D -- no --> F["⚠️ return null
no built-in fallback!"] B -- no --> G{built-in namespace
matches xmlns?} G -- yes --> H["✅ return built-in schema"] G -- no --> I["return null → clear diagnostics"] style E fill:#13241a,stroke:#3fb950,color:#f0f6fc style H fill:#13241a,stroke:#3fb950,color:#f0f6fc style F fill:#2a1416,stroke:#f85149,color:#f0f6fc

The red branch: a registered pattern that matches but points at an unreadable XSD returns null rather than falling through to the built-in schema — so a missing or corrupt bundled XSD silently disables validation for those files. See trade-offs.

4 · Completion request

Read-only and fast — served entirely by engine ②. It prefers the rich auto:// provider built during validation; if that isn't ready yet it builds a partial one on the fly.

sequenceDiagram autonumber participant C as Client participant RH as requestHandlers participant LS as languageService participant SP as SchemaProvider participant CP as XsdCompletionProvider C->>RH: textDocument/completion(pos) RH->>RH: getParsedDoc() — version cache RH->>LS: doComplete(doc, pos, file, path) LS->>SP: resolveSchemaForDocument(file, ns, path) alt auto:// provider exists SP-->>LS: cached full provider else not yet registered SP->>CP: new provider from raw XSD (partial, uncached) CP-->>SP: provider end LS-->>RH: CompletionList RH-->>C: items

5 · Hover (with diagnostic suppression)

Hover is where the two engines meet. To avoid showing misleading hover content for something Xerces flagged as invalid, the handler checks the diagnostics first.

sequenceDiagram autonumber participant C as Client participant RH as requestHandlers participant DH as DiagnosticsHandler participant LS as languageService C->>RH: textDocument/hover(pos) RH->>DH: hasErrorAt(uri, line, char) alt diagnostic covers this position DH-->>RH: true RH-->>C: null — let the diagnostic speak else clean DH-->>RH: false RH->>LS: doHover(doc, pos, file, path) LS-->>RH: hover content RH-->>C: hover end

Reconciliation point: this hasErrorAt() gate is how the two engines are kept from contradicting each other in the UI — engine ① (Xerces) gets the last word over engine ② (completion model).

6 · Configuration reload

Triggered when a new XML file is opened or a workspace folder is added. Everything schema-related is rebuilt from scratch so the re-registered schemas are always in a clean state.

flowchart TD A["onDidChangeConfiguration"] --> B["invalidateAutoSchemas()
drop auto:// validators + providers"] B --> C["diagnosticsHandler.dispose()
destroy ProjectValidators"] C --> D["clearUserAssociations()
wipe the slate"] D --> E["registerSchemas(initializationSchemas)
re-apply bundled 430/440 patterns"] E --> F{initialConfigurationLoaded?} F -- yes --> G["re-validate all open docs"] F -- no --> H["defer"] style D fill:#2a1f10,stroke:#d29922,color:#f0f6fc

Why clear and re-register? Without clearing first, calling registerSchemas repeatedly would pile up duplicate associations. Wiping then re-applying the startup schemas keeps the state identical to a fresh boot.

User Scenarios

What actually happens end-to-end when a developer uses the editor. Both traces use a 4.3.0 project file (df.xml) as the example.

Opening a file for the first time

The developer opens df.xml inside a 430 project. Two things happen in parallel: the completion model is built once from the inlined XSD, and the WASM validator is compiled from the full schema folder. Every edit after this hits both caches.

sequenceDiagram autonumber actor Dev as Developer participant Ed as VS Code participant LS as Language Server participant FS as File System participant W as Xerces WASM Dev->>Ed: opens df.xml (430 project) Ed->>LS: textDocument/didChangeContent Note over LS: debounce 300 ms LS->>LS: pattern "test-mi-430/**/*.xml" → 430 XSD matched rect rgba(20, 50, 20, 0.45) Note over LS,FS: Build completion model — once per schema version LS->>FS: loadReferencedXsds() — follow xs:include chain FS-->>LS: 77 XSD files merged into one 150 KB flat string LS->>LS: XsdCompletionProvider() — parse + walk → 195-element Map Note over LS: cached · future edits skip this entire block end rect rgba(20, 20, 55, 0.45) Note over LS,W: Compile WASM validator — once per schema version LS->>FS: buildFilesMap() — read all 82 XSD files (~836 KB) FS-->>LS: files in JS heap LS->>W: createProjectValidator(filesMap) Note over W: Xerces compiles grammar pool into WASM linear memory W-->>LS: ProjectValidator handle Note over LS: JS text GC-eligible · validator cached end LS->>W: validator.validate(xmlText) W-->>LS: errors / warnings LS-->>Ed: publishDiagnostics — red squiggles appear

Second edit onwards: the two rect blocks above are completely skipped. The LS goes straight to validator.validate() using the cached WASM pool, and completions are served from the already-built 195-element Map — zero disk reads.

Typing in the editor

The developer is editing df.xml and types inside a tag. The completion engine detects the cursor context and does a direct map lookup — no XSD parsing, no disk access.

sequenceDiagram autonumber actor Dev as Developer participant Ed as VS Code participant LS as Language Server participant CP as XsdCompletionProvider (cached Map) rect rgba(30, 30, 10, 0.45) Note over Dev,CP: Context 1 — typing a new child element Dev->>Ed: types < inside <api> body Ed->>LS: textDocument/completion(position) LS->>LS: text before cursor matches /<\w*$/ → element context LS->>LS: findNodeAt() → current node is "api", cursor in content area LS->>CP: getChildren("api") CP-->>LS: ["resource"] LS-->>Ed: suggest resource>$0</resource> end rect rgba(10, 30, 30, 0.45) Note over Dev,CP: Context 2 — typing an attribute Dev->>Ed: types space after <log Ed->>LS: textDocument/completion(position) LS->>LS: text matches open-tag pattern → attribute context LS->>LS: extract tag name = "log" LS->>CP: getAttributes("log") CP-->>LS: [level, category, separator, description] LS-->>Ed: suggest level="$0" category="$0" … end rect rgba(30, 10, 10, 0.45) Note over Dev,CP: Context 3 — closing a tag Dev->>Ed: types >LS: textDocument/completion(position) LS->>LS: text matches /<\/$/ → close-tag context LS->>LS: findNodeAt() → current open element = "log" LS-->>Ed: suggest log> (no schema lookup needed) end

Why it feels instant: by the time the developer types anything, the XsdCompletionProvider is already built and cached from the first didChangeContent event. Every completion request is a single Map.get() call on an in-memory structure — no file I/O, no XSD parsing, no WASM involved.


All six flows at a glance

#TriggerEngine(s)Outcome
1initialize / initializedschemas registered, capabilities advertised
2didChangeContent (debounced 300 ms)① + ②diagnostics published (stale results discarded)
3(internal) findSchemaan XSD chosen, or none
4completioncompletion list
5hover① gates ②hover or null
6didChangeConfigurationcaches rebuilt, docs re-validated