Server Guide¶
This guide covers the IFClite server architecture for production deployments with intelligent caching, parallel processing, and streaming.
Overview¶
The IFClite server processes IFC files on a high-performance Rust backend, providing:
- Content-Addressable Caching - Same file = instant response (skip upload entirely)
- Parallel Processing - Multi-core geometry extraction with Rayon
- Parquet Format - roughly 15-50x smaller payloads than JSON
- SSE Streaming - Progressive geometry for immediate rendering
- Full Data Model - Properties, quantities, and spatial hierarchy computed upfront
Client-Side WASM Is Now the Default
As of the latest release, client-side WASM parsing is the default processing mode. The server is no longer required for basic usage and must be explicitly opted into. Use the server when you need shared caching, parallel processing for very large files, or team-wide deployments.
When to Use Server vs Client¶
| Scenario | Recommendation |
|---|---|
| Single file, one-time view | Client-only (@ifc-lite/parser) |
| Team sharing same files | Server with caching |
| Large models (100+ MB) | Server with streaming |
| Repeat access to same files | Server with caching |
| Offline/embedded apps | Client-only |
| Privacy-sensitive data | Client-only |
Architecture¶
Quick Start¶
1. Start the Server¶
2. Connect from Client¶
import { IfcServerClient } from '@ifc-lite/server-client';
const client = new IfcServerClient({
baseUrl: 'http://localhost:3001'
});
// Health check
const health = await client.health();
console.log('Server status:', health.status);
3. Parse a File¶
// Parquet format (~15x smaller than JSON)
const result = await client.parseParquet(file);
console.log(`Meshes: ${result.meshes.length}`);
console.log(`Cache key: ${result.cache_key}`);
console.log(`From cache: ${result.stats.from_cache}`);
API Endpoints¶
Parse Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/v1/parse |
POST | Full parse, JSON response |
/api/v1/parse/parquet |
POST | Full parse, Parquet response (~15x smaller) |
/api/v1/parse/parquet/optimized |
POST | Optimized Parquet (~50x smaller) |
/api/v1/parse/stream |
POST | Streaming JSON (SSE) |
/api/v1/parse/parquet-stream |
POST | Streaming Parquet (SSE); ?sha256= replays a cache hit with no upload |
/api/v1/parse/metadata |
POST | Quick metadata only (no geometry) |
All parse endpoints that return geometry also surface the 2D symbol stream
(IfcAnnotation + IfcGrid), matching @ifc-lite/parse. The JSON and SSE
responses carry it inline as symbolic_data (in the complete event for the
streaming variants); the binary Parquet transports expose it by cache key via
/api/v1/parse/symbolic/{key} (see below).
Every geometry endpoint's ModelMetadata carries length_unit_scale (factor to
convert model length values to metres, e.g. 0.001 for millimetres) and, when
the model has an IfcMapConversion / IfcProjectedCRS, a georeferencing
object (CRS name, datum, false eastings/northings, orthogonal height, grid-north
rotation, and a local→map 4×4 matrix) — matching @ifc-lite/parse. For the
JSON/SSE endpoints it's on metadata; for the Parquet endpoints it's in the
X-IFC-Metadata header.
Cache Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/v1/cache/check/{hash} |
GET | Check if file is cached (200 or 404) |
/api/v1/cache/geometry/{hash} |
GET | Fetch cached geometry (no upload) |
/api/v1/cache/{key} |
GET | Retrieve cached JSON result |
/api/v1/parse/data-model/{key} |
GET | Fetch cached data model |
/api/v1/parse/symbolic/{key} |
GET | Fetch 2D symbol data (IfcAnnotation + IfcGrid) as JSON |
Utility Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | API information |
/api/v1/health |
GET | Health check (liveness; always open) |
/api/v1/ready |
GET | Readiness probe (503 while the memory breaker is shedding load) |
/api/v1/metrics |
GET | Prometheus text metrics (registered only when IFC_METRICS_ENABLED=1) |
Optional bearer-token auth
When IFC_SERVER_API_TOKEN (or API_TOKEN) is set, all parse and cache
endpoints require an Authorization: Bearer <token> header and return 401
otherwise. The /, /api/v1/health, and /api/v1/ready probes stay open
so health checks keep working. When unset (the default), all routes are
open and the server logs a startup warning that it is unauthenticated.
Client SDK¶
IfcServerClient¶
import { IfcServerClient } from '@ifc-lite/server-client';
const client = new IfcServerClient({
baseUrl: 'http://localhost:3001',
timeout: 300000 // 5 minutes (default)
});
Parse Methods¶
parseParquet (Recommended)¶
Best for most use cases - roughly 15x smaller payloads than JSON.
const result = await client.parseParquet(file);
// Result contains:
// - cache_key: string
// - meshes: MeshData[]
// - metadata: ModelMetadata
// - stats: ProcessingStats
// - parquet_stats: { payload_size, decode_time_ms }
// - data_model?: ArrayBuffer (properties, quantities, hierarchy)
parseParquetOptimized¶
Roughly 50x smaller payloads using integer quantization (0.1mm precision).
const result = await client.parseParquetOptimized(file);
// Same as parseParquet but with:
// - Integer vertex quantization (0.1mm precision)
// - Byte colors (0-255 instead of 0-1)
// - Mesh deduplication (instancing), rotation-aware: repeated occurrences of
// one shape at different orientations (e.g. IfcMappedItem reuse) share one
// template mesh, placed per instance by an origin + a 3x3 rotation
parseParquetStream¶
Progressive rendering for large files (>50MB).
import type { MeshData as ServerMeshData } from '@ifc-lite/server-client';
// Server meshes use snake_case fields (express_id); map them into the
// renderer's camelCase MeshData shape before uploading.
const toRendererMesh = (m: ServerMeshData) => ({
expressId: m.express_id,
ifcType: m.ifc_type,
positions: m.positions,
normals: m.normals,
indices: m.indices,
color: m.color,
});
const streamResult = await client.parseParquetStream(file, (batch) => {
// Called for each geometry batch
renderer.addMeshes(batch.meshes.map(toRendererMesh));
});
// Or use async iterator
for await (const event of client.parseStream(file)) {
switch (event.type) {
case 'start':
console.log(`Processing ~${event.total_estimate} entities`);
break;
case 'batch':
renderer.addMeshes(event.meshes.map(toRendererMesh));
break;
case 'progress':
console.log(`${event.processed}/${event.total}`);
break;
case 'complete':
console.log(`Done in ${event.stats.total_time_ms}ms`);
break;
}
}
getMetadata¶
Quick metadata extraction without geometry processing.
const metadata = await client.getMetadata(file);
// Returns:
// - schema_version: string (e.g. 'IFC2X3', 'IFC4', 'IFC4X3')
// - entity_count: number
// - geometry_count: number
// - file_size: number
// - oversized_id_count?: number // records the scan refused (#3395)
// - malformed_record_found?: boolean // the scan stopped early (#3695)
The last two report that entity_count covers less than the file declares:
oversized_id_count is how many records were skipped for an instance name
too large to represent, and malformed_record_found means the scan hit a
record with no terminating ; (an unterminated string or comment, or a
truncated upload) and stopped there, so the count covers only the bytes
before the break.
Both are optional, and absence is not the same as zero. A server older than these fields sends neither, which means "not scanned for" — never "clean". Branch on presence before reading the value:
const metadata = await client.getMetadata(file);
if (metadata.malformed_record_found) {
console.warn('This file is truncated — the counts above are partial.');
} else if (metadata.malformed_record_found === undefined) {
console.warn('This server does not check; the counts may still be partial.');
}
Cache Methods¶
Checking Cache Before Upload¶
parseParquet hashes the file client-side and checks the server cache
before uploading, so re-parsing the same file skips the upload automatically:
// Automatic: parseParquet computes the SHA-256 and does the cache check
// internally, returning the cached result without re-uploading on a hit.
const result = await client.parseParquet(file);
To retrieve a previously processed result later, keep its cache_key. Re-calling
parseParquet serves the geometry straight from the cache; fetch the cached data
model (properties + spatial hierarchy) with fetchDataModel:
// Geometry: re-calling parseParquet returns the cached result without re-upload.
const result = await client.parseParquet(file);
// Data model (properties + hierarchy) for a known cache key:
const dataModelBuffer = await client.fetchDataModel(result.cache_key);
getCached(key) is the lower-level lookup for the JSON parse() cache and
returns a ParseResponse; it is not the retrieval path for Parquet geometry.
Fetching Data Model¶
Properties and spatial hierarchy are computed in parallel and cached:
import { decodeDataModel } from '@ifc-lite/server-client';
const result = await client.parseParquet(file);
// Data model might still be processing
// Use polling to wait for it
const dataModel = await client.fetchDataModel(result.cache_key);
if (dataModel) {
const decoded = await decodeDataModel(dataModel);
console.log(`Entities: ${decoded.entities.size}`);
console.log(`Property sets: ${decoded.propertySets.size}`);
}
Fetching Symbolic Data¶
The JSON (parse) and streaming endpoints return the 2D symbol stream
(IfcAnnotation + IfcGrid) inline as symbolic_data. The binary Parquet
endpoints (parseParquet, parseParquetOptimized) can't carry it inline —
fetch it by cache key instead:
const result = await client.parseParquet(file);
const symbols = await client.fetchSymbolic(result.cache_key);
if (symbols) {
console.log(`Grid axes: ${symbols.grid_axes.length}`);
console.log(`Annotations: ${symbols.polylines.length} polylines, ${symbols.texts.length} labels`);
}
Utility Methods¶
// Health check
const health = await client.health();
// Uploads are gzip-compressed automatically by parse()/parseParquet(),
// so no manual compression step is needed.
// Check Parquet decoder availability
const available = await client.isParquetSupported();
Data Model¶
The server computes a complete data model including entities, property sets,
quantity sets, relationships, spatial hierarchy, and — matching @ifc-lite/parse
— per-element classifications (IfcClassificationReference), materials
(IfcMaterialLayerSet layers with metre thicknesses), and documents
(IfcDocumentReference). The latter three are exposed as flat, element-keyed
arrays on the decoded DataModel (classifications, materials, documents)
and decode to empty arrays when served by an older server/cache.
Entities¶
interface EntityMetadata {
entity_id: number;
type_name: string;
global_id?: string;
name?: string;
description?: string;
object_type?: string;
has_geometry: boolean;
}
Properties¶
interface PropertySet {
pset_id: number;
pset_name: string;
properties: Property[];
}
interface Property {
property_name: string;
property_value: string;
property_type: string;
}
Quantities¶
interface QuantitySet {
qset_id: number;
qset_name: string;
method_of_measurement?: string;
quantities: Quantity[];
}
interface Quantity {
quantity_name: string;
quantity_value: number;
quantity_type: string; // 'Area', 'Volume', 'Length', etc.
}
Relationships¶
One row per (relating, related) pair: an IfcRel* record naming several
related objects is flattened into one row each, so the same rel_id can appear
on several rows.
interface Relationship {
rel_type: string; // e.g. 'IFCRELAGGREGATES', as declared in the file
relating_id: number;
related_id: number;
rel_id?: number; // express id of the IfcRel entity
}
rel_id is what a Parquet or DuckDB export writes as RelId, and what
identifies the record to delete or edit. Two cases where it is not a live
express id:
- Absent (
undefined) when the server predates thedatamodel-v6payload, which is where the column was added. Consumers fall back to0; the viewer warns once naming the column, since every id reading 0 otherwise looks like a normal graph. 0on the syntheticTYPEHASPROPERTYSETSrows. Those are read offIfcTypeObject.HasPropertySetsto attach a type's own property sets, and no IFC entity declares them, so there is no id to carry.
Spatial Hierarchy¶
interface SpatialHierarchy {
nodes: SpatialNode[];
project_id: number;
element_to_storey: Map<number, number>;
element_to_building: Map<number, number>;
element_to_site: Map<number, number>;
element_to_space: Map<number, number>;
}
interface SpatialNode {
entity_id: number;
parent_id: number;
level: number;
path: string;
type_name: string;
name?: string;
elevation?: number;
children_ids: number[];
element_ids: number[];
}
Parquet Format¶
The server uses Apache Parquet for efficient binary serialization.
Standard Format¶
- Mesh Table: express_id, ifc_type, vertex/index offsets, RGBA color
- Vertex Table: x, y, z (Float32), nx, ny, nz (Float32)
- Index Table: i0, i1, i2 (Uint32 triangle indices)
Shared-shape layout (opt-in)¶
POST /api/v1/parse/parquet and /parse/parquet-stream accept
?parquet_layout=shared-shapes. Occurrences of one shape — repeated furniture,
pipe runs, structural members — then share a single block of vertices instead of
each carrying a full copy, and the mesh table gains nine rot0..rot8 columns
(row-major 3x3, Float32) placing each occurrence:
Both values in the same Y-up metres frame as the positions. On a model with repeats this is typically 2.4x to 4.8x smaller; a model with nothing to share is unchanged apart from the identity rotation columns.
It is opt-in, and it must stay that way. The layout renders incorrectly on a client that does not apply the rotation — every occurrence of a shared shape lands at the template's placement — and this format carries no version marker such a client could reject, so the server cannot produce it unless the request says the client understands it. Omit the parameter and you get the layout above, byte for byte.
Two further consequences:
origin_x/y/zmust be read. On the default layout from a stock server it is zero on every row, so a decoder could ignore it and be accidentally right. Undershared-shapesit carries the placement.- Send the same parameter to
/cache/check/{hash}and/cache/geometry/{hash}. The two layouts are cached separately (-parquet-v5/-parquet-v6) and never cross-serve, so a check that omits it answers about the other entry.@ifc-lite/server-clientdoes this for you.
Optimized Format¶
- Instance Table: entity_id, ifc_type, mesh_index, material_index
- Mesh Table: Deduplicated unique geometries
- Material Table: Deduplicated RGBA colors (Uint8)
- Vertex Table: Quantized integers (0.1mm precision)
- Index Table: Triangle indices
Decoding on Client¶
import {
decodeParquetGeometry,
decodeOptimizedParquetGeometry,
decodeDataModel
} from '@ifc-lite/server-client';
// Standard Parquet
const meshes = await decodeParquetGeometry(parquetBuffer);
// Optimized Parquet (with vertex dequantization)
const optimizedMeshes = await decodeOptimizedParquetGeometry(parquetBuffer, 10000);
// Data model
const dataModel = await decodeDataModel(dataModelBuffer);
Caching Strategy¶
Content-Addressable Keys¶
Cache keys are derived from file content:
# {filter} is the opening filter (e.g. "default"); a non-default tessellation
# quality appends a "-q{level}" suffix after it
{SHA256}-{filter}-parquet-v5 # Geometry (default layout)
{SHA256}-{filter}-parquet-v6 # Geometry (parquet_layout=shared-shapes)
{SHA256}-{filter}-parquet-metadata-v4 # Metadata header
{SHA256}-{filter}-datamodel-v6 # Properties & hierarchy
{SHA256}-{filter}-symbolic-v1 # 2D symbol stream
# POST /parse/parquet/optimized has its own pair (issue #3889): the optimized
# payload is quantized and deduplicated, so a hit on one route must never
# satisfy the other. Both pairs are built from the same geometry pipeline, so
# a bump of -parquet-v5 almost always needs a bump of -parquet-optimized-v1.
{SHA256}-{filter}-parquet-optimized-v1 # Optimized geometry
{SHA256}-{filter}-parquet-optimized-metadata-v1 # Optimized metadata header
The two geometry keys are LAYOUTS, not versions: they coexist, and a request
reaches one or the other according to its parquet_layout parameter (below).
They never cross-serve, because the shared-shape layout renders incorrectly on
a client that does not know to apply its rotation columns.
The {SHA256} half is what a client can supply itself, and two endpoints let
it: GET /api/v1/cache/check/{hash} and POST /api/v1/parse/parquet-stream?sha256=
(above). A client-supplied hash is a SELECTOR for entries that already exist and
nothing more: it never causes a parse or a write, and on a request that also
carries a file body it is discarded in favour of hashing that body. The stream
probe additionally requires it to be 64 lowercase hex characters and answers
400 otherwise, because the value is concatenated into the keys above and a
caller-shaped hash would otherwise be a caller-shaped key. /cache/check and
/cache/geometry do not check the shape; a malformed hash there simply names a
key nobody wrote, and they answer 404.
Each suffix is bumped whenever the payload it names changes shape: a column
added to or removed from its tables, or a change in what an existing column
means. Without the bump a warm cache replays the old blob, the client decodes
it cleanly as if an older server had answered, and the change is silently
absent. The keys above are built in apps/server/src/routes/parse/cache_keys.rs;
that module is the single definition of each one.
Cache Flow¶
Cache Benefits¶
| Scenario | Without Cache | With Cache |
|---|---|---|
| First load of a file | Full parse + geometry extraction | Full parse + geometry extraction |
| Repeat load of the same file | Full parse again | Serve pre-computed Parquet from disk |
| Upload | Always | Skipped entirely on a hit (hash check first) |
On a cache hit the server does no parsing at all: the response is a disk read plus network transfer, so repeat loads are typically orders of magnitude faster than the first load.
Server Configuration¶
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
PORT |
8080 | Server port |
RUST_LOG |
info (+ debug for server/http spans) | Log filter (error, warn, info, debug, trace) |
MAX_FILE_SIZE_MB |
500 | Maximum upload size in MB |
WORKER_THREADS |
CPU cores | Parallel processing threads |
CACHE_DIR |
./.cache (/app/cache in Docker) |
Cache directory path |
REQUEST_TIMEOUT_SECS |
300 | Request timeout in seconds |
INITIAL_BATCH_SIZE |
100 | Streaming initial batch size |
MAX_BATCH_SIZE |
1000 | Streaming maximum batch size |
CACHE_MAX_AGE_DAYS |
7 | Cache retention in days |
CORS_ORIGINS |
localhost dev origins | Allowed CORS origins (comma-separated, * for all) |
IFC_SERVER_API_TOKEN |
unset | Optional bearer token for parse/cache routes (falls back to API_TOKEN) |
IFC_MAX_CONCURRENT_PARSES |
WORKER_THREADS |
Parse jobs admitted at once (CPU gate) |
IFC_MEM_BUDGET_MB |
70% of detected memory limit | Upload bytes admitted at once; 0 disables the memory gate |
IFC_ADMISSION_QUEUE_DEPTH |
2 x WORKER_THREADS |
Requests allowed to queue for an admission permit |
IFC_ADMISSION_QUEUE_TIMEOUT_SECS |
5 | Longest a queued request waits for a permit |
IFC_MEM_SHED_PCT |
85 | RSS percentage of the budget above which new parses are shed |
IFC_METRICS_ENABLED |
false | Expose GET /api/v1/metrics |
Docker Compose¶
version: '3.8'
services:
ifc-lite-server:
image: ghcr.io/ltplus-ag/ifc-lite-server:latest
ports:
- "3001:8080"
environment:
- RUST_LOG=info
- MAX_FILE_SIZE_MB=500
- WORKER_THREADS=8
- CACHE_MAX_AGE_DAYS=30
volumes:
- ifc-cache:/app/cache
volumes:
ifc-cache:
Adding Health Checks
For orchestration systems requiring health checks, the server exposes
GET /api/v1/health. If your runtime image includes curl or wget:
Production Deployment¶
For production, consider:
- Reverse Proxy - Use nginx or Traefik for SSL termination
- Persistent Cache - Mount a volume for the cache directory
- Resource Limits - Set memory/CPU limits based on expected file sizes
- Monitoring - Enable debug logging for troubleshooting
# Railway deployment
railway up
# Fly.io deployment
fly deploy
# Kubernetes
kubectl apply -f k8s/deployment.yaml
Streaming¶
Dynamic Batch Sizing¶
The server uses dynamic batch sizing for optimal streaming:
- Initial batch: 100 entities (fast first frame)
- Growth: Increases based on processing speed
- Maximum: 1000 entities per batch
Streaming Flow¶
Skipping the upload on a cache hit¶
The cache key is the SHA-256 of the bytes the server receives, so the upload
used to finish before the cache could be consulted: a hit on a 40 MB model
still paid for 40 MB on the wire. POST /api/v1/parse/parquet-stream therefore
accepts the hash on its own (issue #3901):
# No body. The hash names the entry; the query names which entry.
curl -X POST "$SERVER/api/v1/parse/parquet-stream?sha256=$SHA&parquet_layout=shared-shapes"
- 200 with the SSE stream: everything the replay needs was cached, and it replays exactly the events a live parse emits, batch by batch.
- 404: nothing cached under that key. Resend the request with the multipart
filebody, which is the normal path. This is the same statusGET /api/v1/cache/check/{hash}uses for "upload it". - 400: the
sha256value is not 64 lowercase hex characters.
Two rules keep the parameter honest:
- A hash sent alongside a body is ignored. The received bytes decide which entry is read and written, always. A client cannot store one file's geometry under another file's key.
- The hash only selects. It can not cause anything to be parsed or written; a key with nothing behind it is a 404, never a parse.
Send the same opening_filter, tessellation_quality and parquet_layout you
would send with the file. They are part of the cache identity, so a hash paired
with a different layout asks about a different entry.
@ifc-lite/server-client does this for you: parseParquetStream hashes the
file locally, probes, and uploads on the 404. It also uploads when the probe is
not answered at all, which is what a server built before this change does (its
route still requires a multipart body, so a bodyless POST is rejected as a bad
boundary), when a proxy refuses the shape, when admission sheds the probe, or
when it times out. A probe gets a 5 second budget for its headers, not the
client's full request timeout, because a probe that is not fast is not worth
the upload it is delaying. Pass { skipCacheProbe: true } to go straight to
the upload.
Compressed uploads never hit the probe
The server keys on the bytes it parses, which is what it receives after
unwrapping .ifcZIP or gzip. A probe carrying the hash of a still-packed
file names a key nothing was written under, so it answers 404 and the
client uploads. That is correct, just not a saving. GET /cache/check
behaves the same way, for the same reason.
Client-Side Streaming¶
import type { MeshData as ServerMeshData } from '@ifc-lite/server-client';
// Server meshes use snake_case fields; map to the renderer's shape.
const toRendererMesh = (m: ServerMeshData) => ({
expressId: m.express_id,
ifcType: m.ifc_type,
positions: m.positions,
normals: m.normals,
indices: m.indices,
color: m.color,
});
// Using callback
await client.parseParquetStream(file, (batch) => {
// batch.meshes are already decoded server MeshData
renderer.addMeshes(batch.meshes.map(toRendererMesh));
});
// Using async iterator
for await (const event of client.parseStream(file)) {
if (event.type === 'batch') {
renderer.addMeshes(event.meshes.map(toRendererMesh));
}
}
Performance Optimization¶
Server-Side¶
- Parallel Processing - Geometry and data model extracted concurrently
- Rayon Thread Pool - Utilizes all CPU cores
- Streaming Caching - Meshes accumulated during stream, cached at end
- Lazy Data Model - Client polls for data model while rendering geometry
Client-Side¶
- Hash Check First - Skip upload if file is cached
- Parquet Decoding - WASM-based decoder for fast parsing
- Progressive Rendering - Render batches as they arrive
- Background Polling - Fetch data model while geometry renders
Network¶
- Gzip Compression - Applied automatically on upload by
parse()/parseParquet() - Parquet Format - roughly 15-50x smaller than JSON
- SSE Streaming - No polling overhead
Error Handling¶
Server Errors¶
try {
const result = await client.parseParquet(file);
} catch (error) {
if (error.status === 413) {
console.error('File too large - increase MAX_FILE_SIZE_MB');
} else if (error.status === 408) {
console.error('Timeout - try streaming for large files');
} else if (error.status === 500) {
console.error('Server error:', error.message);
}
}
Streaming Errors¶
A stream can fail in two different ways, and they surface differently.
The server can report a failure, which arrives as an error event:
for await (const event of client.parseStream(file)) {
if (event.type === 'error') {
console.error('Stream error:', event.message);
break;
}
}
Or the stream can simply stop — the connection drops, or the server dies
mid-parse — in which case no error event is ever sent. There is nothing to
inspect in the loop, so parseStream() throws once the body ends without a
terminal event. Handle both:
try {
for await (const event of client.parseStream(file)) {
if (event.type === 'error') {
console.error('Stream error:', event.message);
break;
}
}
} catch (error) {
// "Stream ended without a complete event (connection dropped or the server
// failed mid-parse)" — the parse did NOT finish, and no partial result
// should be treated as one.
console.error('Stream did not complete:', error);
}
Checking only for event.type === 'error' leaves the second case unhandled: the
loop ends early and the throw escapes uncaught. parseStreamToParquet() raises
the same error for the same condition.
Connection Errors¶
try {
await client.health();
} catch (error) {
if (error.message.includes('ECONNREFUSED')) {
console.error('Server not running');
} else if (error.message.includes('timeout')) {
console.error('Server not responding');
}
}
Next Steps¶
- Parsing Guide - Client-side parsing details
- Rendering Guide - WebGPU rendering features
- API Reference - Complete API documentation
- Architecture - System design details