@openmbee/opensysml 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +462 -0
- package/dist/browser/index.d.ts +12 -0
- package/dist/browser/index.js +34 -0
- package/dist/core/capabilities.d.ts +94 -0
- package/dist/core/capabilities.js +130 -0
- package/dist/core/connection.d.ts +79 -0
- package/dist/core/connection.js +110 -0
- package/dist/core/errors.d.ts +98 -0
- package/dist/core/errors.js +100 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +10 -0
- package/dist/core/model.d.ts +139 -0
- package/dist/core/model.js +339 -0
- package/dist/core/package.d.ts +4 -0
- package/dist/core/package.js +4 -0
- package/dist/core/status.d.ts +15 -0
- package/dist/core/status.js +94 -0
- package/dist/core/transport.d.ts +10 -0
- package/dist/core/transport.js +69 -0
- package/dist/core/values.d.ts +271 -0
- package/dist/core/values.js +687 -0
- package/dist/generated/sysml_pb.d.ts +5668 -0
- package/dist/generated/sysml_pb.js +837 -0
- package/dist/node/binary.d.ts +127 -0
- package/dist/node/binary.js +564 -0
- package/dist/node/index.d.ts +31 -0
- package/dist/node/index.js +101 -0
- package/dist/node/service.d.ts +31 -0
- package/dist/node/service.js +238 -0
- package/dist/node/signing.d.ts +55 -0
- package/dist/node/signing.js +167 -0
- package/package.json +77 -0
- package/release-digests.json +60 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { Connection } from "./connection.js";
|
|
2
|
+
import type { ModelDiagnostic } from "./errors.js";
|
|
3
|
+
import type { Instance as PbInstance, ParseFileRequest, SymbolInfo } from "../generated/sysml_pb.js";
|
|
4
|
+
import { type SysMLValue } from "./values.js";
|
|
5
|
+
/** How a source is parsed. */
|
|
6
|
+
export interface ParseOptions {
|
|
7
|
+
/** Language of inline content ("sysml" or "kerml"); requires `inline_language`. */
|
|
8
|
+
language?: string;
|
|
9
|
+
/** Reject the OpenSysML notation extensions; requires `strict_conformance`. */
|
|
10
|
+
strict?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Where an expression is evaluated. */
|
|
13
|
+
export interface EvalOptions {
|
|
14
|
+
/** FQN of the symbol whose scope the expression's names resolve in. */
|
|
15
|
+
context?: string;
|
|
16
|
+
/** FQN of a usage to instantiate and evaluate against; requires `evaluate_subject`. */
|
|
17
|
+
subject?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A model the service has parsed and holds under its hash. */
|
|
20
|
+
export declare class Model {
|
|
21
|
+
readonly connection: Connection;
|
|
22
|
+
/** The hash the service holds this model under; every later call names it. */
|
|
23
|
+
readonly hash: string;
|
|
24
|
+
/** Diagnostics the parse reported, in the order the service reported them. */
|
|
25
|
+
readonly diagnostics: readonly ModelDiagnostic[];
|
|
26
|
+
private readonly rootSymbol;
|
|
27
|
+
private readonly ownsConnection;
|
|
28
|
+
private constructor();
|
|
29
|
+
/** Adopts a model the service already holds, by the hash it holds it under. */
|
|
30
|
+
static adopt(connection: Connection, hash: string): Model;
|
|
31
|
+
/** Whether this model was parsed here, and so knows its root symbol. */
|
|
32
|
+
get parsed(): boolean;
|
|
33
|
+
/** The model's root namespace. An adopted model has none: look symbols up by id. */
|
|
34
|
+
get root(): ModelSymbol;
|
|
35
|
+
/** Parses a source over `connection`. Used by Connection.load / Connection.loads. */
|
|
36
|
+
static parse(connection: Connection, source: Pick<ParseFileRequest, "source">, options?: ParseOptions, ownsConnection?: boolean): Promise<Model>;
|
|
37
|
+
/** Whether the parse reported an error-severity diagnostic. */
|
|
38
|
+
get hasErrors(): boolean;
|
|
39
|
+
/** Evaluates a SysML expression against this model. */
|
|
40
|
+
eval(expression: string, options?: EvalOptions): Promise<SysMLValue>;
|
|
41
|
+
/** Looks a symbol up by short name, FQN or id; throws when the model declares none. */
|
|
42
|
+
symbol(name: string): Promise<ModelSymbol>;
|
|
43
|
+
/** Looks a symbol up by its qualified name, in one call. */
|
|
44
|
+
symbolById(id: string): Promise<ModelSymbol>;
|
|
45
|
+
/** Looks a symbol up by short name, FQN or id, breadth-first from the root. */
|
|
46
|
+
find(name: string): Promise<ModelSymbol | undefined>;
|
|
47
|
+
/** Every symbol of the model, breadth-first from the root. */
|
|
48
|
+
walk(): AsyncGenerator<ModelSymbol>;
|
|
49
|
+
/** Instantiates a part or usage, by short name, FQN or id. */
|
|
50
|
+
instantiate(name: string): Promise<InstanceTree>;
|
|
51
|
+
/** Releases the model. Closes the connection only when this model opened it. */
|
|
52
|
+
close(): Promise<void>;
|
|
53
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
54
|
+
/** Whether a name is one the service can resolve directly, without a search. */
|
|
55
|
+
private looksQualified;
|
|
56
|
+
/** The names of the model closest to one it has not got, best first. */
|
|
57
|
+
private nearNames;
|
|
58
|
+
}
|
|
59
|
+
/** Static type facts about a symbol, as the service reports them. */
|
|
60
|
+
export interface TypeFacts {
|
|
61
|
+
declared: string;
|
|
62
|
+
resolvedId: string;
|
|
63
|
+
resolvedKind: string;
|
|
64
|
+
primitive: string;
|
|
65
|
+
primitiveSource: string;
|
|
66
|
+
quantity: boolean;
|
|
67
|
+
unit: string;
|
|
68
|
+
}
|
|
69
|
+
/** One attribute of a symbol, with its declared default when it has one. */
|
|
70
|
+
export interface AttributeFacts {
|
|
71
|
+
name: string;
|
|
72
|
+
type: string;
|
|
73
|
+
unit: string;
|
|
74
|
+
value: SysMLValue;
|
|
75
|
+
}
|
|
76
|
+
/** One specialization relationship a symbol declares or inherits. */
|
|
77
|
+
export interface SpecializationFacts {
|
|
78
|
+
kind: string;
|
|
79
|
+
declared: string;
|
|
80
|
+
targetId: string;
|
|
81
|
+
targetKind: string;
|
|
82
|
+
}
|
|
83
|
+
/** A symbol of a loaded model. */
|
|
84
|
+
export declare class ModelSymbol {
|
|
85
|
+
readonly id: string;
|
|
86
|
+
readonly name: string;
|
|
87
|
+
readonly kind: string;
|
|
88
|
+
readonly metadata: Readonly<Record<string, string>>;
|
|
89
|
+
readonly childIds: readonly string[];
|
|
90
|
+
readonly attributes: readonly AttributeFacts[];
|
|
91
|
+
readonly type: TypeFacts | undefined;
|
|
92
|
+
readonly multiplicity: {
|
|
93
|
+
lower: string;
|
|
94
|
+
upper: string;
|
|
95
|
+
} | undefined;
|
|
96
|
+
readonly specializations: readonly SpecializationFacts[];
|
|
97
|
+
/** Library attributes the service did not send, when it withheld any. */
|
|
98
|
+
readonly withheldLibraryAttributes: number;
|
|
99
|
+
private readonly connection;
|
|
100
|
+
private readonly modelHash;
|
|
101
|
+
constructor(connection: Connection, modelHash: string, info: SymbolInfo);
|
|
102
|
+
/** The symbols this one owns, fetched one call each. */
|
|
103
|
+
children(): Promise<ModelSymbol[]>;
|
|
104
|
+
/** The value of one attribute, or undefined when the symbol declares no such attribute. */
|
|
105
|
+
attribute(name: string): AttributeFacts | undefined;
|
|
106
|
+
}
|
|
107
|
+
/** One feature's value on an instantiated object. */
|
|
108
|
+
export type FeatureValue = {
|
|
109
|
+
kind: "single";
|
|
110
|
+
value: SysMLValue;
|
|
111
|
+
materialized: boolean;
|
|
112
|
+
} | {
|
|
113
|
+
kind: "many";
|
|
114
|
+
values: SysMLValue[];
|
|
115
|
+
} | {
|
|
116
|
+
kind: "error";
|
|
117
|
+
error: string;
|
|
118
|
+
};
|
|
119
|
+
/** An instantiated object. */
|
|
120
|
+
export declare class Instance {
|
|
121
|
+
readonly id: bigint;
|
|
122
|
+
readonly typeId: string;
|
|
123
|
+
readonly features: ReadonlyMap<string, FeatureValue>;
|
|
124
|
+
constructor(instance: PbInstance);
|
|
125
|
+
/** The value of one feature, or undefined when the object has no such feature. */
|
|
126
|
+
get(name: string): FeatureValue | undefined;
|
|
127
|
+
}
|
|
128
|
+
/** What one instantiation produced: the root object and every object it owns. */
|
|
129
|
+
export declare class InstanceTree {
|
|
130
|
+
readonly root: Instance;
|
|
131
|
+
/** Every object the instantiation produced, the root included when the service sent it. */
|
|
132
|
+
readonly all: readonly Instance[];
|
|
133
|
+
readonly diagnostics: readonly ModelDiagnostic[];
|
|
134
|
+
constructor(root: Instance, all: readonly Instance[], diagnostics: readonly ModelDiagnostic[]);
|
|
135
|
+
/** The value of one feature of the root object. */
|
|
136
|
+
get(name: string): FeatureValue | undefined;
|
|
137
|
+
/** The object with this id, or undefined when the instantiation produced none. */
|
|
138
|
+
byId(id: bigint): Instance | undefined;
|
|
139
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// The ergonomic surface: a loaded model, its symbols, and the objects it
|
|
2
|
+
// instantiates. Everything here is built on Connection.rpc.
|
|
3
|
+
import { CAPABILITY_EVALUATE_SUBJECT, CAPABILITY_INLINE_LANGUAGE, CAPABILITY_STRICT_CONFORMANCE, requireCapability, upgradeRemedy, } from "./capabilities.js";
|
|
4
|
+
import { EvaluationError, OpenSysMLError, ParseError, SymbolNotFoundError } from "./errors.js";
|
|
5
|
+
import { callRpc } from "./status.js";
|
|
6
|
+
import { decodeValue } from "./values.js";
|
|
7
|
+
/** How alike two names must be for one to be suggested for the other. */
|
|
8
|
+
const NEAR_ENOUGH = 0.6;
|
|
9
|
+
/** Symbols a search for near names reads before giving up, so a big model is cheap. */
|
|
10
|
+
const NEAR_SEARCH_LIMIT = 500;
|
|
11
|
+
/** How alike two names are, from 0 to 1, by edit distance over the longer one. */
|
|
12
|
+
function similarity(left, right) {
|
|
13
|
+
const longest = Math.max(left.length, right.length);
|
|
14
|
+
if (longest === 0) {
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
return 1 - editDistance(left, right) / longest;
|
|
18
|
+
}
|
|
19
|
+
/** Levenshtein distance, one row of the matrix at a time. */
|
|
20
|
+
function editDistance(left, right) {
|
|
21
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
22
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
23
|
+
const current = [row];
|
|
24
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
25
|
+
const substitution = (previous[column - 1] ?? 0) + (left[row - 1] === right[column - 1] ? 0 : 1);
|
|
26
|
+
const deletion = (previous[column] ?? 0) + 1;
|
|
27
|
+
const insertion = (current[column - 1] ?? 0) + 1;
|
|
28
|
+
current.push(Math.min(substitution, deletion, insertion));
|
|
29
|
+
}
|
|
30
|
+
previous = current;
|
|
31
|
+
}
|
|
32
|
+
return previous[right.length] ?? 0;
|
|
33
|
+
}
|
|
34
|
+
/** A model the service has parsed and holds under its hash. */
|
|
35
|
+
export class Model {
|
|
36
|
+
connection;
|
|
37
|
+
/** The hash the service holds this model under; every later call names it. */
|
|
38
|
+
hash;
|
|
39
|
+
/** Diagnostics the parse reported, in the order the service reported them. */
|
|
40
|
+
diagnostics;
|
|
41
|
+
rootSymbol;
|
|
42
|
+
ownsConnection;
|
|
43
|
+
constructor(init) {
|
|
44
|
+
this.connection = init.connection;
|
|
45
|
+
this.hash = init.hash;
|
|
46
|
+
this.rootSymbol = init.root;
|
|
47
|
+
this.diagnostics = init.diagnostics;
|
|
48
|
+
this.ownsConnection = init.ownsConnection;
|
|
49
|
+
}
|
|
50
|
+
/** Adopts a model the service already holds, by the hash it holds it under. */
|
|
51
|
+
static adopt(connection, hash) {
|
|
52
|
+
return new Model({ connection, hash, diagnostics: [], ownsConnection: false });
|
|
53
|
+
}
|
|
54
|
+
/** Whether this model was parsed here, and so knows its root symbol. */
|
|
55
|
+
get parsed() {
|
|
56
|
+
return this.rootSymbol !== undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The model's root namespace. An adopted model has none: look symbols up by id. */
|
|
59
|
+
get root() {
|
|
60
|
+
if (this.rootSymbol === undefined) {
|
|
61
|
+
throw new OpenSysMLError(`model ${this.hash} was adopted by hash, not parsed here, so its root is unknown; ` +
|
|
62
|
+
"look a symbol up by its qualified name with symbolById()");
|
|
63
|
+
}
|
|
64
|
+
return this.rootSymbol;
|
|
65
|
+
}
|
|
66
|
+
/** Parses a source over `connection`. Used by Connection.load / Connection.loads. */
|
|
67
|
+
static async parse(connection, source, options = {}, ownsConnection = false) {
|
|
68
|
+
if (options.language !== undefined) {
|
|
69
|
+
requireCapability(connection.info, CAPABILITY_INLINE_LANGUAGE, upgradeRemedy(CAPABILITY_INLINE_LANGUAGE));
|
|
70
|
+
}
|
|
71
|
+
if (options.strict === true) {
|
|
72
|
+
requireCapability(connection.info, CAPABILITY_STRICT_CONFORMANCE, upgradeRemedy(CAPABILITY_STRICT_CONFORMANCE));
|
|
73
|
+
}
|
|
74
|
+
const response = await callRpc(connection.rpc.parseFile({
|
|
75
|
+
source: source.source,
|
|
76
|
+
...(options.language === undefined ? {} : { language: options.language }),
|
|
77
|
+
...(options.strict === undefined ? {} : { strictConformance: options.strict }),
|
|
78
|
+
}, connection.callOptions()), source.source.case === "filePath" ? "file" : "model");
|
|
79
|
+
const diagnostics = response.diagnostics.map(decodeDiagnostic);
|
|
80
|
+
if (response.error !== "") {
|
|
81
|
+
throw new ParseError(response.error, diagnostics);
|
|
82
|
+
}
|
|
83
|
+
if (response.root === undefined) {
|
|
84
|
+
throw new ParseError("the service parsed the source but returned no root symbol", diagnostics);
|
|
85
|
+
}
|
|
86
|
+
return new Model({
|
|
87
|
+
connection,
|
|
88
|
+
hash: response.modelHash,
|
|
89
|
+
root: new ModelSymbol(connection, response.modelHash, response.root),
|
|
90
|
+
diagnostics,
|
|
91
|
+
ownsConnection,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Whether the parse reported an error-severity diagnostic. */
|
|
95
|
+
get hasErrors() {
|
|
96
|
+
return this.diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
97
|
+
}
|
|
98
|
+
/** Evaluates a SysML expression against this model. */
|
|
99
|
+
async eval(expression, options = {}) {
|
|
100
|
+
if (options.subject !== undefined) {
|
|
101
|
+
requireCapability(this.connection.info, CAPABILITY_EVALUATE_SUBJECT, upgradeRemedy(CAPABILITY_EVALUATE_SUBJECT));
|
|
102
|
+
}
|
|
103
|
+
const response = await callRpc(this.connection.rpc.evaluate({
|
|
104
|
+
modelHash: this.hash,
|
|
105
|
+
expression,
|
|
106
|
+
...(options.context === undefined ? {} : { contextSymbolId: options.context }),
|
|
107
|
+
...(options.subject === undefined ? {} : { subjectSymbolId: options.subject }),
|
|
108
|
+
}, this.connection.callOptions()));
|
|
109
|
+
if (response.error !== "") {
|
|
110
|
+
throw new EvaluationError(response.error, "unspecified", response.diagnostics.map(decodeDiagnostic));
|
|
111
|
+
}
|
|
112
|
+
return decodeValue(response.result);
|
|
113
|
+
}
|
|
114
|
+
/** Looks a symbol up by short name, FQN or id; throws when the model declares none. */
|
|
115
|
+
async symbol(name) {
|
|
116
|
+
if (this.looksQualified(name)) {
|
|
117
|
+
return this.symbolById(name);
|
|
118
|
+
}
|
|
119
|
+
// A short name is searched for from the root, which an adopted model has not
|
|
120
|
+
// got; the service resolves a name the model declares at its top level.
|
|
121
|
+
if (this.rootSymbol === undefined) {
|
|
122
|
+
return this.symbolById(name);
|
|
123
|
+
}
|
|
124
|
+
const found = await this.find(name);
|
|
125
|
+
if (found === undefined) {
|
|
126
|
+
throw new SymbolNotFoundError(name, await this.nearNames(name));
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
/** Looks a symbol up by its qualified name, in one call. */
|
|
131
|
+
async symbolById(id) {
|
|
132
|
+
const response = await callRpc(this.connection.rpc.getSymbol({ modelHash: this.hash, symbolId: id }, this.connection.callOptions()));
|
|
133
|
+
if (response.symbol === undefined) {
|
|
134
|
+
throw new SymbolNotFoundError(id);
|
|
135
|
+
}
|
|
136
|
+
return new ModelSymbol(this.connection, this.hash, response.symbol);
|
|
137
|
+
}
|
|
138
|
+
/** Looks a symbol up by short name, FQN or id, breadth-first from the root. */
|
|
139
|
+
async find(name) {
|
|
140
|
+
for await (const symbol of this.walk()) {
|
|
141
|
+
if (symbol.name === name || symbol.id === name) {
|
|
142
|
+
return symbol;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/** Every symbol of the model, breadth-first from the root. */
|
|
148
|
+
async *walk() {
|
|
149
|
+
const queue = [this.root];
|
|
150
|
+
while (queue.length > 0) {
|
|
151
|
+
const current = queue.shift();
|
|
152
|
+
if (current === undefined) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
yield current;
|
|
156
|
+
queue.push(...(await current.children()));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Instantiates a part or usage, by short name, FQN or id. */
|
|
160
|
+
async instantiate(name) {
|
|
161
|
+
const id = this.looksQualified(name) ? name : (await this.symbol(name)).id;
|
|
162
|
+
const response = await callRpc(this.connection.rpc.instantiate({ modelHash: this.hash, symbolId: id }, this.connection.callOptions()));
|
|
163
|
+
if (response.error !== "") {
|
|
164
|
+
throw new EvaluationError(response.error, "unspecified", response.diagnostics.map(decodeDiagnostic));
|
|
165
|
+
}
|
|
166
|
+
if (response.instance === undefined) {
|
|
167
|
+
throw new EvaluationError(`the service instantiated ${id} but returned no object`);
|
|
168
|
+
}
|
|
169
|
+
return new InstanceTree(new Instance(response.instance), response.instances.map((instance) => new Instance(instance)), response.diagnostics.map(decodeDiagnostic));
|
|
170
|
+
}
|
|
171
|
+
/** Releases the model. Closes the connection only when this model opened it. */
|
|
172
|
+
async close() {
|
|
173
|
+
if (this.ownsConnection) {
|
|
174
|
+
await this.connection.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async [Symbol.asyncDispose]() {
|
|
178
|
+
await this.close();
|
|
179
|
+
}
|
|
180
|
+
/** Whether a name is one the service can resolve directly, without a search. */
|
|
181
|
+
looksQualified(name) {
|
|
182
|
+
return name.includes("::") || name === this.rootSymbol?.id;
|
|
183
|
+
}
|
|
184
|
+
/** The names of the model closest to one it has not got, best first. */
|
|
185
|
+
async nearNames(name) {
|
|
186
|
+
const scored = [];
|
|
187
|
+
let seen = 0;
|
|
188
|
+
for await (const symbol of this.walk()) {
|
|
189
|
+
// An unnamed symbol, the root among them, is near nothing.
|
|
190
|
+
if (symbol.name === "") {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const score = similarity(name.toLowerCase(), symbol.name.toLowerCase());
|
|
194
|
+
if (score >= NEAR_ENOUGH) {
|
|
195
|
+
scored.push({ id: symbol.id, score });
|
|
196
|
+
}
|
|
197
|
+
seen += 1;
|
|
198
|
+
if (seen === NEAR_SEARCH_LIMIT) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
scored.sort((left, right) => right.score - left.score);
|
|
203
|
+
return scored.slice(0, 3).map((one) => one.id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** A symbol of a loaded model. */
|
|
207
|
+
export class ModelSymbol {
|
|
208
|
+
id;
|
|
209
|
+
name;
|
|
210
|
+
kind;
|
|
211
|
+
metadata;
|
|
212
|
+
childIds;
|
|
213
|
+
attributes;
|
|
214
|
+
type;
|
|
215
|
+
multiplicity;
|
|
216
|
+
specializations;
|
|
217
|
+
/** Library attributes the service did not send, when it withheld any. */
|
|
218
|
+
withheldLibraryAttributes;
|
|
219
|
+
connection;
|
|
220
|
+
modelHash;
|
|
221
|
+
constructor(connection, modelHash, info) {
|
|
222
|
+
this.connection = connection;
|
|
223
|
+
this.modelHash = modelHash;
|
|
224
|
+
this.id = info.id;
|
|
225
|
+
this.name = info.name;
|
|
226
|
+
this.kind = info.kind;
|
|
227
|
+
this.metadata = { ...info.metadata };
|
|
228
|
+
this.childIds = [...info.childIds];
|
|
229
|
+
this.attributes = info.attributes.map((attribute) => ({
|
|
230
|
+
name: attribute.name,
|
|
231
|
+
type: attribute.type,
|
|
232
|
+
unit: attribute.unit,
|
|
233
|
+
value: decodeValue(attribute.value),
|
|
234
|
+
}));
|
|
235
|
+
this.type =
|
|
236
|
+
info.typeInfo === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: {
|
|
239
|
+
declared: info.typeInfo.declared,
|
|
240
|
+
resolvedId: info.typeInfo.resolvedId,
|
|
241
|
+
resolvedKind: info.typeInfo.resolvedKind,
|
|
242
|
+
primitive: info.typeInfo.primitive,
|
|
243
|
+
primitiveSource: info.typeInfo.primitiveSource,
|
|
244
|
+
quantity: info.typeInfo.quantity,
|
|
245
|
+
unit: info.typeInfo.unit,
|
|
246
|
+
};
|
|
247
|
+
this.multiplicity =
|
|
248
|
+
info.multiplicity === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: { lower: info.multiplicity.lower, upper: info.multiplicity.upper };
|
|
251
|
+
this.specializations = info.specializations.map((specialization) => ({
|
|
252
|
+
kind: specialization.kind,
|
|
253
|
+
declared: specialization.declared,
|
|
254
|
+
targetId: specialization.targetId,
|
|
255
|
+
targetKind: specialization.targetKind,
|
|
256
|
+
}));
|
|
257
|
+
this.withheldLibraryAttributes = info.withheldLibraryAttributes;
|
|
258
|
+
}
|
|
259
|
+
/** The symbols this one owns, fetched one call each. */
|
|
260
|
+
async children() {
|
|
261
|
+
const children = [];
|
|
262
|
+
for (const id of this.childIds) {
|
|
263
|
+
const response = await callRpc(this.connection.rpc.getSymbol({ modelHash: this.modelHash, symbolId: id }, this.connection.callOptions()));
|
|
264
|
+
if (response.symbol !== undefined) {
|
|
265
|
+
children.push(new ModelSymbol(this.connection, this.modelHash, response.symbol));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return children;
|
|
269
|
+
}
|
|
270
|
+
/** The value of one attribute, or undefined when the symbol declares no such attribute. */
|
|
271
|
+
attribute(name) {
|
|
272
|
+
return this.attributes.find((attribute) => attribute.name === name);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** An instantiated object. */
|
|
276
|
+
export class Instance {
|
|
277
|
+
id;
|
|
278
|
+
typeId;
|
|
279
|
+
features;
|
|
280
|
+
constructor(instance) {
|
|
281
|
+
this.id = instance.id;
|
|
282
|
+
this.typeId = instance.typeSymbolId;
|
|
283
|
+
const features = new Map();
|
|
284
|
+
for (const [name, value] of Object.entries(instance.featureValues)) {
|
|
285
|
+
features.set(name, decodeFeatureValue(value));
|
|
286
|
+
}
|
|
287
|
+
this.features = features;
|
|
288
|
+
}
|
|
289
|
+
/** The value of one feature, or undefined when the object has no such feature. */
|
|
290
|
+
get(name) {
|
|
291
|
+
return this.features.get(name);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/** What one instantiation produced: the root object and every object it owns. */
|
|
295
|
+
export class InstanceTree {
|
|
296
|
+
root;
|
|
297
|
+
/** Every object the instantiation produced, the root included when the service sent it. */
|
|
298
|
+
all;
|
|
299
|
+
diagnostics;
|
|
300
|
+
constructor(root, all, diagnostics) {
|
|
301
|
+
this.root = root;
|
|
302
|
+
this.all = all;
|
|
303
|
+
this.diagnostics = diagnostics;
|
|
304
|
+
}
|
|
305
|
+
/** The value of one feature of the root object. */
|
|
306
|
+
get(name) {
|
|
307
|
+
return this.root.get(name);
|
|
308
|
+
}
|
|
309
|
+
/** The object with this id, or undefined when the instantiation produced none. */
|
|
310
|
+
byId(id) {
|
|
311
|
+
return this.all.find((instance) => instance.id === id) ?? (this.root.id === id ? this.root : undefined);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function decodeFeatureValue(value) {
|
|
315
|
+
if (value.error !== "") {
|
|
316
|
+
return { kind: "error", error: value.error };
|
|
317
|
+
}
|
|
318
|
+
if (value.values.length > 0) {
|
|
319
|
+
return { kind: "many", values: value.values.map((element) => decodeValue(element)) };
|
|
320
|
+
}
|
|
321
|
+
return { kind: "single", value: decodeValue(value.value), materialized: value.materialized };
|
|
322
|
+
}
|
|
323
|
+
function decodeDiagnostic(diagnostic) {
|
|
324
|
+
const span = diagnostic.span;
|
|
325
|
+
return {
|
|
326
|
+
severity: diagnostic.severity,
|
|
327
|
+
message: diagnostic.message,
|
|
328
|
+
code: diagnostic.code,
|
|
329
|
+
...(span === undefined
|
|
330
|
+
? {}
|
|
331
|
+
: {
|
|
332
|
+
file: span.file,
|
|
333
|
+
startLine: span.startLine,
|
|
334
|
+
startColumn: span.startCol,
|
|
335
|
+
endLine: span.endLine,
|
|
336
|
+
endColumn: span.endCol,
|
|
337
|
+
}),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The npm name this client is published under; package.json's "name" must match it. */
|
|
2
|
+
export declare const PACKAGE_NAME = "@openmbee/opensysml";
|
|
3
|
+
/** Prefix of the per-platform packages that carry sysml-grpc: `${prefix}<os>-<cpu>`. */
|
|
4
|
+
export declare const PLATFORM_PACKAGE_PREFIX = "@openmbee/opensysml-sysml-grpc-";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The npm name this client is published under; package.json's "name" must match it. */
|
|
2
|
+
export const PACKAGE_NAME = "@openmbee/opensysml";
|
|
3
|
+
/** Prefix of the per-platform packages that carry sysml-grpc: `${prefix}<os>-<cpu>`. */
|
|
4
|
+
export const PLATFORM_PACKAGE_PREFIX = `${PACKAGE_NAME}-sysml-grpc-`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Code } from "@connectrpc/connect";
|
|
2
|
+
import { OpenSysMLError, ServiceError } from "./errors.js";
|
|
3
|
+
/** The name of a status code, as a scenario and an error message spell it. */
|
|
4
|
+
export declare function statusName(code: Code): string;
|
|
5
|
+
/**
|
|
6
|
+
* What a NOT_FOUND names when the service's message does not say. The call site
|
|
7
|
+
* knows what it asked about: a path, or a model held under a hash.
|
|
8
|
+
*/
|
|
9
|
+
export type NotFoundSubject = "model" | "file";
|
|
10
|
+
/** Translates a failed call into the error for its status. */
|
|
11
|
+
export declare function fromRpcError(error: unknown, notFound?: NotFoundSubject): OpenSysMLError;
|
|
12
|
+
/** Translates a failed handshake, which says which service would not answer. */
|
|
13
|
+
export declare function fromHandshakeError(error: unknown, origin: string): ServiceError;
|
|
14
|
+
/** Awaits a call, translating whatever status it fails with. */
|
|
15
|
+
export declare function callRpc<T>(call: Promise<T>, notFound?: NotFoundSubject): Promise<T>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The client boundary for the service's statuses: a failed call is translated
|
|
2
|
+
// into this client's error hierarchy, keeping the ConnectError as its cause.
|
|
3
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
4
|
+
import { InvalidRequestError, ModelFileNotFoundError, ModelNotFoundError, OpenSysMLError, ServiceError, ServiceTimeoutError, SymbolNotFoundError, UnsupportedOperationError, } from "./errors.js";
|
|
5
|
+
/** A status code as the service's conformance scenarios spell it. */
|
|
6
|
+
const STATUS_NAMES = new Map([
|
|
7
|
+
[Code.Canceled, "CANCELLED"],
|
|
8
|
+
[Code.Unknown, "UNKNOWN"],
|
|
9
|
+
[Code.InvalidArgument, "INVALID_ARGUMENT"],
|
|
10
|
+
[Code.DeadlineExceeded, "DEADLINE_EXCEEDED"],
|
|
11
|
+
[Code.NotFound, "NOT_FOUND"],
|
|
12
|
+
[Code.AlreadyExists, "ALREADY_EXISTS"],
|
|
13
|
+
[Code.PermissionDenied, "PERMISSION_DENIED"],
|
|
14
|
+
[Code.ResourceExhausted, "RESOURCE_EXHAUSTED"],
|
|
15
|
+
[Code.FailedPrecondition, "FAILED_PRECONDITION"],
|
|
16
|
+
[Code.Aborted, "ABORTED"],
|
|
17
|
+
[Code.OutOfRange, "OUT_OF_RANGE"],
|
|
18
|
+
[Code.Unimplemented, "UNIMPLEMENTED"],
|
|
19
|
+
[Code.Internal, "INTERNAL"],
|
|
20
|
+
[Code.Unavailable, "UNAVAILABLE"],
|
|
21
|
+
[Code.DataLoss, "DATA_LOSS"],
|
|
22
|
+
[Code.Unauthenticated, "UNAUTHENTICATED"],
|
|
23
|
+
]);
|
|
24
|
+
/** The name of a status code, as a scenario and an error message spell it. */
|
|
25
|
+
export function statusName(code) {
|
|
26
|
+
return STATUS_NAMES.get(code) ?? String(code);
|
|
27
|
+
}
|
|
28
|
+
// Statuses that name a distinct failure. Anything else stays a ServiceError, so
|
|
29
|
+
// a status this client has never seen still arrives inside the hierarchy.
|
|
30
|
+
const CODE_ERRORS = new Map([
|
|
31
|
+
[Code.InvalidArgument, InvalidRequestError],
|
|
32
|
+
[Code.FailedPrecondition, InvalidRequestError],
|
|
33
|
+
[Code.OutOfRange, InvalidRequestError],
|
|
34
|
+
[Code.DeadlineExceeded, ServiceTimeoutError],
|
|
35
|
+
[Code.Canceled, ServiceTimeoutError],
|
|
36
|
+
[Code.Unimplemented, UnsupportedOperationError],
|
|
37
|
+
]);
|
|
38
|
+
/** Translates a failed call into the error for its status. */
|
|
39
|
+
export function fromRpcError(error, notFound = "model") {
|
|
40
|
+
// A capability refusal or a closed connection is already this client's own.
|
|
41
|
+
if (error instanceof OpenSysMLError) {
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
const connectError = ConnectError.from(error);
|
|
45
|
+
const status = statusName(connectError.code);
|
|
46
|
+
const message = connectError.rawMessage === ""
|
|
47
|
+
? `the sysml-grpc service failed the call with ${status}`
|
|
48
|
+
: connectError.rawMessage;
|
|
49
|
+
if (connectError.code === Code.NotFound) {
|
|
50
|
+
return notFoundError(message, status, notFound, connectError);
|
|
51
|
+
}
|
|
52
|
+
const cls = CODE_ERRORS.get(connectError.code) ?? ServiceError;
|
|
53
|
+
const described = connectError.code === Code.Unavailable ? `sysml-grpc service unavailable: ${message}` : message;
|
|
54
|
+
return new cls(described, { cause: connectError, code: status });
|
|
55
|
+
}
|
|
56
|
+
/** Translates a failed handshake, which says which service would not answer. */
|
|
57
|
+
export function fromHandshakeError(error, origin) {
|
|
58
|
+
const connectError = ConnectError.from(error);
|
|
59
|
+
const cls = CODE_ERRORS.get(connectError.code) ?? ServiceError;
|
|
60
|
+
return new cls(`the sysml-grpc service at ${origin} did not answer: ${connectError.message}`, {
|
|
61
|
+
cause: connectError,
|
|
62
|
+
code: statusName(connectError.code),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Awaits a call, translating whatever status it fails with. */
|
|
66
|
+
export async function callRpc(call, notFound = "model") {
|
|
67
|
+
try {
|
|
68
|
+
return await call;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw fromRpcError(error, notFound);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// The service answers NOT_FOUND for a source file it cannot read, a model hash
|
|
75
|
+
// it no longer holds and a symbol a model does not declare; it says which.
|
|
76
|
+
function notFoundError(message, status, notFound, cause) {
|
|
77
|
+
const lowered = message.toLowerCase();
|
|
78
|
+
if (lowered.includes("file not found") || lowered.includes("no such file")) {
|
|
79
|
+
return new ModelFileNotFoundError(message, { cause, code: status });
|
|
80
|
+
}
|
|
81
|
+
if (lowered.includes("model not found")) {
|
|
82
|
+
return new ModelNotFoundError(message, { cause, code: status });
|
|
83
|
+
}
|
|
84
|
+
if (lowered.includes("symbol not found")) {
|
|
85
|
+
return new SymbolNotFoundError(symbolNameIn(message));
|
|
86
|
+
}
|
|
87
|
+
const cls = notFound === "file" ? ModelFileNotFoundError : ModelNotFoundError;
|
|
88
|
+
return new cls(message, { cause, code: status });
|
|
89
|
+
}
|
|
90
|
+
function symbolNameIn(message) {
|
|
91
|
+
const marker = "symbol not found: ";
|
|
92
|
+
const at = message.lastIndexOf(marker);
|
|
93
|
+
return at === -1 ? message : message.slice(at + marker.length);
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Interceptor } from "@connectrpc/connect";
|
|
2
|
+
import type { Encoding, TransportOptions } from "./connection.js";
|
|
3
|
+
/** Turns `host:port`, `http://host:port` or a full URL into a Connect base URL. */
|
|
4
|
+
export declare function baseUrl(address: string): string;
|
|
5
|
+
/** The interceptors a connection installs for its headers and its response tap. */
|
|
6
|
+
export declare function interceptors(options: TransportOptions): Interceptor[];
|
|
7
|
+
/** Protobuf unless the caller asked for JSON. */
|
|
8
|
+
export declare function encodingOf(options: TransportOptions): Encoding;
|
|
9
|
+
/** The deadline every call carries, checked: one that cannot elapse is a mistake. */
|
|
10
|
+
export declare function timeoutOf(options: TransportOptions): number | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Pieces both entry points share: how an address becomes a base URL, and the
|
|
2
|
+
// interceptors a connection installs.
|
|
3
|
+
import { OpenSysMLError } from "./errors.js";
|
|
4
|
+
/** Turns `host:port`, `http://host:port` or a full URL into a Connect base URL. */
|
|
5
|
+
export function baseUrl(address) {
|
|
6
|
+
const trimmed = address.trim();
|
|
7
|
+
if (trimmed === "") {
|
|
8
|
+
throw new OpenSysMLError("the service address is empty");
|
|
9
|
+
}
|
|
10
|
+
const withScheme = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
11
|
+
let url;
|
|
12
|
+
try {
|
|
13
|
+
url = new URL(withScheme);
|
|
14
|
+
}
|
|
15
|
+
catch (cause) {
|
|
16
|
+
throw new OpenSysMLError(`${JSON.stringify(address)} is not a service address`, { cause });
|
|
17
|
+
}
|
|
18
|
+
if (url.port === "" && !/^https?:\/\//.test(trimmed)) {
|
|
19
|
+
throw new OpenSysMLError(`${JSON.stringify(address)} names no port; a sysml-grpc address is host:port`);
|
|
20
|
+
}
|
|
21
|
+
return url.origin + url.pathname.replace(/\/$/, "");
|
|
22
|
+
}
|
|
23
|
+
/** The interceptors a connection installs for its headers and its response tap. */
|
|
24
|
+
export function interceptors(options) {
|
|
25
|
+
const built = [];
|
|
26
|
+
const headers = options.headers;
|
|
27
|
+
if (headers !== undefined) {
|
|
28
|
+
built.push((next) => (request) => {
|
|
29
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
30
|
+
request.header.set(name, value);
|
|
31
|
+
}
|
|
32
|
+
return next(request);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const tap = options.onResponse;
|
|
36
|
+
if (tap !== undefined) {
|
|
37
|
+
built.push(responseTap(tap));
|
|
38
|
+
}
|
|
39
|
+
return built;
|
|
40
|
+
}
|
|
41
|
+
const ENCODINGS = new Set(["protobuf", "json"]);
|
|
42
|
+
/** Protobuf unless the caller asked for JSON. */
|
|
43
|
+
export function encodingOf(options) {
|
|
44
|
+
const encoding = options.encoding ?? "protobuf";
|
|
45
|
+
if (!ENCODINGS.has(encoding)) {
|
|
46
|
+
throw new OpenSysMLError(`${JSON.stringify(encoding)} is not a body encoding; a connection carries "protobuf" or "json"`);
|
|
47
|
+
}
|
|
48
|
+
return encoding;
|
|
49
|
+
}
|
|
50
|
+
/** The deadline every call carries, checked: one that cannot elapse is a mistake. */
|
|
51
|
+
export function timeoutOf(options) {
|
|
52
|
+
const timeoutMs = options.timeoutMs;
|
|
53
|
+
if (timeoutMs === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
57
|
+
throw new OpenSysMLError(`timeoutMs is a deadline in milliseconds and must be positive, not ${JSON.stringify(timeoutMs)}`);
|
|
58
|
+
}
|
|
59
|
+
return timeoutMs;
|
|
60
|
+
}
|
|
61
|
+
function responseTap(tap) {
|
|
62
|
+
return (next) => async (request) => {
|
|
63
|
+
const response = await next(request);
|
|
64
|
+
if (!response.stream) {
|
|
65
|
+
tap({ method: request.method.name, response: response.message });
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
};
|
|
69
|
+
}
|