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.
{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.
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
| Thing | Scope | When created | When reused |
|---|---|---|---|
JS AST (parseXMLDocument) | Per validation run | Only if pattern match failed or WASM threw | Cached within the same run via getXmlDoc() |
Completion schema (auto://) | Per file | First edit of that file | All later edits of the same file |
WASM validator (ProjectValidator) | Per XSD folder | First file using that schema | All other files using the same schema |
Example: two projects, three files
- Open project1/proxy.xml Pattern matches → 440 XSD. No parse needed. Completion schema registered for
proxy.xml. WASM validator for 440 created and cached. - Open project2/sequence.xml Pattern matches → 430 XSD. No parse needed. Completion schema registered for
sequence.xml. WASM validator for 430 created and cached. - 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
- Debounce Each edit cancels the previous timer; validation fires 300 ms after the last keystroke.
- Snapshot version
validateAndSendcapturesdocument.versionon entry — the live document keeps advancing while async work runs. - Resolve pass 1
SchemaAssociatortries to match the file path against registered patterns — no parsing needed. Succeeds in the common case. - 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.
- Register completion schema (once per file) The XSD import graph is loaded and registered under
auto://<path>. Skipped on later edits viahasSchema(). - Get validator (once per XSD folder) A
ProjectValidatoris created with every.xsdfile compiled into WASM — or the cached one is returned immediately. - 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). - Deduplicate & map Structural errors appear in both parse and schema error lists — duplicates removed, then mapped to LSP ranges.
- Version guard If
document.versionmoved on during validation, the result is discarded. - 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.
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.
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.
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.
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.
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.
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.
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
| # | Trigger | Engine(s) | Outcome |
|---|---|---|---|
| 1 | initialize / initialized | — | schemas registered, capabilities advertised |
| 2 | didChangeContent (debounced 300 ms) | ① + ② | diagnostics published (stale results discarded) |
| 3 | (internal) findSchema | — | an XSD chosen, or none |
| 4 | completion | ② | completion list |
| 5 | hover | ① gates ② | hover or null |
| 6 | didChangeConfiguration | — | caches rebuilt, docs re-validated |