This page covers what @openmbee/opensysml exports, how its two entry points differ, and
where its surface stops. To choose between the clients, see
client libraries; for a task-oriented walkthrough, see
guide chapter 9. The client's own
notes on packaging and its conformance run are in
client/node/README.md.
npm install @openmbee/opensysmlThe package is published on npm. From a checkout, build it with
npm install && npm run build in client/node.
| import | reaches | private child service |
|---|---|---|
@openmbee/opensysml |
Node, over Connect with protobuf bodies | yes, by default |
@openmbee/opensysml/browser |
a page, over fetch (@connectrpc/connect-web) |
no — a browser spawns nothing |
Both re-export the isomorphic core; the browser entry point requires an
address, since there is nothing to fall back to.
import { connect, load, loads } from "@openmbee/opensysml";
await using connection = await connect(); // private child of this process
const model = await connection.loads("package Demo { part def Car; }");load(path) and loads(source) are the one-shot forms: each opens a connection
of its own and closes it with the model. connect() is the longer-lived form, and
every model parsed over it shares one service and one parse cache. Connection
and Model both implement Symbol.asyncDispose, so await using closes them;
close() is the explicit form and is safe to call twice.
ConnectOptions extends TransportOptions:
| option | effect |
|---|---|
address |
host:port or a URL of a service to use; without it, a private child is started |
encoding |
"protobuf" (default) or "json"; JSON costs ~6x the service's CPU on large answers |
protocol |
"connect" (default) or "grpc"; { protocol: "grpc", encoding: "json" } is refused, not downgraded |
timeoutMs |
deadline applied to every call the connection makes |
headers |
extra headers sent with every call |
onResponse |
called with each response, for logging, metrics or a conformance runner |
version |
the version the service must report, else connecting fails with StaleServiceError |
requireCapabilities |
capabilities the service must advertise, else connecting fails |
$OPENSYSML_SERVICE=host:port is the environment form of address. A connection
to a service this client did not start is only disconnected from on close(),
never stopped.
const model = await connection.load("model.sysml");
model.hash; // what the service holds it under
model.diagnostics; // in the order the service reported them
model.hasErrors; // any error-severity diagnostic
const car = await model.symbol("Demo::Car"); // short name, FQN or id
await car.children(); // its members, one call each
for await (const symbol of model.walk()) {} // breadth-first from the root
const value = await model.eval("2 + 2"); // SysMLValue
const mass = await model.eval("mass", { subject: "Demo::sedan" });
const tree = await model.instantiate("Demo::Car");
tree.get("wheels"); // a FeatureValue of the root object
tree.byId(id); // any object the instantiation producedconnection.model(hash) adopts a model the service already holds, which lets a
hash pass between processes; an adopted model has no root symbol, so symbols are
looked up by qualified name, and the service answers NOT_FOUND once it has
evicted the model. symbol() searches breadth-first when given a short name and
raises SymbolNotFoundError naming near misses for any name the model has not
got; symbolById() is the single call for a name the service can resolve
directly, reporting a miss with no suggestions.
ParseOptions are language ("sysml" or "kerml", for inline content) and
strictConformance; both are capability-gated, and the client checks before it
calls. strict remains as a deprecated alias of strictConformance; passing
both with different values is refused. To raise on parse errors, call
model.raiseForErrors() on the returned model.
Every oneof the service answers with arrives as a union to switch on, rather
than a message with optional fields:
switch (value.kind) {
case "int": value.value; // bigint, never lossy, beyond int64 too
case "real": value.value; // number
case "complex": value.value.real; value.value.imaginary; // one value, not two floats
case "boolean":
case "string": value.value;
case "quantity": value.magnitude; value.unit;
case "measurementRef": value.unit; value.unitTerm; value.unitId; // a bare unit and its reduction
case "array": value.dimensions; value.elements; // row-major SysMLValue[]
case "vector": value.components; // Magnitude[]: int | real, kept apart
case "vectorQuantity": value.components; // QuantityValue[], one unit each
case "set": value.elements; // SysMLValue[], each once, unordered
case "tensorQuantity": value.dimensions; value.components; // any rank, row-major QuantityValue[]
case "function": value.calcId; value.selfId; // a calc held as a value; selfId when read off an object
case "metaobject": value.elementId; value.metaclassId; // x meta KerML::Feature: the element reflected on
case "enum": value.value.name; value.value.value; // literal/enumeration ids, and a `high = 3` literal's scalar
case "instance": value.id; // an object in the same tree
case "sequence": value.elements; // SysMLValue[]
case "undetermined": value.reason; value.countLower; value.countUpper; // left open by the model
case "infinity": break; // the unbounded `*`
case "null": value.reason; // evaluated, no value
case "unset": break; // declared, never given one
case "absent": break; // the service sent no value at all
}unset, undetermined and absent are distinct on purpose: the first is a feature the
model leaves without a value, the second an answer the model leaves open (read, never sent),
the third a field the answer did not carry. SysMLVerdict
(holds / fails / undecided) and FeatureValue (single / many / error)
are unions of the same shape; every verdict arm carries a standing (engine, strength,
bounds, plus reported and reached — the bounds the engine stopped at),
empty from a service without the engines capability; CalcResult,
AnalysisResult, Validation and SweepTable read the same fields through
engine, strength and bounds getters. Integers are bigint,
because the service's int64 does not fit a number and an exact comparison would
otherwise be a lie. decodeValue, decodeVerdict, decodeStanding and formatValue are
exported for a caller decoding a response it obtained itself.
Everything derives from OpenSysMLError, so the family can be caught without
knowing its members.
| error | what happened |
|---|---|
ServiceError |
the service could not be reached, started, or answered nothing usable |
ServiceUnavailableError |
the service was unreachable, refused the stream, or died before answering; in a browser also a fetch that never answered (dead address, CORS refusal, mid-call loss), which arrives as UNKNOWN |
ServiceStartError |
a private child failed to start, or died while it was needed |
StaleServiceError |
the running service reports another version than version asked for |
ClosedConnectionError |
the connection was closed and cannot be used again |
ParseError |
a file could not be read, or its content did not parse; carries diagnostics |
EvaluationError |
the call succeeded and the answer reports a model failure |
ExecutionError |
an execution the service ran failed; carries diagnostics |
WrongKindError |
a verification or analysis named a symbol of another kind |
AnalysisRunError |
an analysis run failed before it could report |
ConversionError |
the service could not write the notation asked for |
MigrationError |
the service could not read the SysML v1 model, so nothing of it was migrated |
UnsupportedValueError |
the service sent a value this version of the client cannot decode |
QueryError / DocumentQueryError |
a Query or runDocumentQuery failed in-band |
EditError |
an edit was refused; subclasses (NoEditsError, EditTargetError, InvalidEditError, IllegalMemberKindError, RenameReferencedError, OverlappingEditsError, EditResultError, OwnerNotFoundError, OwnerNotNamespaceError, MemberNameTakenError, DeleteReferencedError, OwnerInsideTargetError, MoveReferencedError, ReferencedElsewhereError) catch one kind of refusal |
TypeMismatchError / InstanceTypeError / FeatureValueError |
a typed view read a feature of another kind, an instance of another type, or a slot that is an error |
SymbolNotFoundError |
the model declares no such symbol |
MissingCapabilityError |
the service does not advertise a capability the call needs |
DownloadError |
a release binary could not be downloaded or installed |
ChecksumMismatchError |
a download's digest contradicts the one expected of it |
UnpinnedReleaseError / UnsignedReleaseError / ManifestSignatureError |
nothing pins the release, nothing signs it, or a signature does not verify |
The ParseError/EvaluationError split against ServiceError is the one the
conformance suite draws: an expression that will not evaluate is a successful call
carrying an error, not a service problem.
Clients negotiate on the capability names GetServerInfo reports, never on the
version string:
import { CAPABILITY_EVALUATE_SUBJECT } from "@openmbee/opensysml";
if (connection.info.has(CAPABILITY_EVALUATE_SUBJECT)) {
await model.eval("mass", { subject: "Demo::sedan" });
}The client checks the advertised list before making a gated call, so
MissingCapabilityError names the service, its version and how to get one that
has the capability, without a round trip. A service too old to answer
GetServerInfo at all is still usable: connection.info.answered is false and
its capability list is empty. Capabilities that only describe how a response is
populated omit the fields they name rather than refusing the call.
The binary comes from an optional per-platform npm package
(@openmbee/opensysml-sysml-grpc-{linux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64}),
selected by npm from its os/cpu metadata, with no postinstall script.
Resolution order is $OPENSYSML_BINARY, that package,
~/.opensysml/bin/sysml-grpc (the cache the Python, Java and Rust clients share),
then sysml-grpc on $PATH. resolveBinary() downloads a release into that
cache when nothing above resolved, verifying it against the digests pinned in the
published release-digests.json, else against the release's sigstore-signed
SHA256SUMS.txt; a release neither pins nor signs is refused unless
$OPENSYSML_ALLOW_UNPINNED_DOWNLOAD accepts same-origin trust explicitly.
client/node/README.md documents the download and
its trust model in full.
A private child is started with -port 0 -health-port 0 -report-address -exit-with-parent, and the client reads the address from its first stdout line,
so no port is chosen, probed or retried. One child serves every connection of a
thread — a worker_threads worker gets its own — and stops when the last
connection closes. It cannot be orphaned: the client holds the write end of the
child's stdin and never writes to it, so the kernel closes that pipe however this
process dies, SIGKILL included, and the child exits at end of file.
import { connect } from "@openmbee/opensysml/browser";
await using connection = await connect({ address: "https://sysml.example.com" });Two limits to plan for: the service must allow the page's exact origin
(-cors-allowed-origins https://app.example.com, never *) and be served over
TLS for an HTTPS page to reach it; and connect-go does not implement the base64
grpc-web-text variant, which this fetch-based client does not need but a
grpc-web client requiring -text would.
Beside the model reads above, the client covers every RPC the service offers:
connection.parseSourcesparses several documents as one model (SourceDocument.file/inlineper document;model.documents,model.roots);connection.convertand Node'ssave(target, path)write a model, file or conversion out insysml,kerml,turtleorapi-json; a SysML v1 model (xmi,uml,mdzip, byfromFormator by extension) is refused with anInvalidRequestErrornamingmigrate, since it is migrated, not converted;connection.migratemigrates a SysML v1 model from apathor inlinecontentbytes tosysmlorttl, withfromFormat,report,results,layoutPath/layoutContent,imageBaseUrlandstrictas the command's companion flags; theMigrationcarries the content, aMigrationReport(summary,mapped/approximated/unmapped/skipped,entriesandtextwhen asked,byVerdict(verdict)),resultsand the imagefiles, whichsave(migration, path)writes beside the notation; a v2fromFormatis refused with a pointer atconvert;model.query(OSLC or structured),model.runDocumentQuerywithElementRef/ObjectRefbindings,model.renderDocumentto Markdown or HTML;model.executeAction/executeStatefor runs andexploreAction/exploreState/exploreAnalysisfor explorations of every schedule — the two families refuse each other'sschedule, as the wire does;model.verifyConstraint/verifyRequirement/verifySatisfaction/satisfied,validateInstance,calc,runAnalysis,runSweep(ranges asparameter → [from, to]or[from, to, step]), all takingengine,subject,question, arguments ornamedArgumentsas the call allows, andconnection.listEnginesnames what the service answers with;model.edit()builds anEditor/Bodybatch of source-preserving edits, applied atomically byapply(), with per-operation capability gating and the typedEditErrorfamily for refusals;connection.applyEdits(modelHash, ops)is the wire form, takingEditOperationmessages. A model of several documents is edited in one batch — the result'sdocumentslists every document the batch rewrote by parse name andapplied[].documentnames where each change landed — andacceptDocuments/documentoptions onapplyEditsmap the request fields directly;opensysml-generate(thebinof the package) writes a module of typed views over a model's definitions, stamped with a hash of the source; a classextendsits first base and re-declares the other bases' features as getters,fromInstanceaccepts subtypes, and--checkfails on drift.
connection.rpc remains the escape hatch — the generated Connect client — and
SysMLService is exported for a caller building its own.
npm run conformance -- --allow-skips --report report.json runs the
language-neutral suite through the public API, and emits the report shape
tools/cmd/conformance emits. 158 scenarios per protocol over grpc, connect and
connect-json: 155 pass and 3 are skipped, being the requests the public API
refuses eagerly (a ParseFile naming no source, a ParseSources naming no
document or two alike); a refusal the client makes in the service's own words and
status (a v1 model offered to convert, a v2 one to migrate) runs as that status.
--mutate <name> corrupts a response on its way through the client and each
mutation must make a scenario fail, which is what keeps the run from being
vacuous.