@openmbee/opensysml 0.9.2 → 0.10.0
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 +355 -33
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.js +16 -0
- package/dist/browser/wasm-worker.d.ts +1 -0
- package/dist/browser/wasm-worker.js +2 -0
- package/dist/browser/wasm.d.ts +17 -0
- package/dist/browser/wasm.js +94 -0
- package/dist/core/arguments.d.ts +4 -0
- package/dist/core/arguments.js +19 -0
- package/dist/core/capabilities.d.ts +90 -9
- package/dist/core/capabilities.js +111 -10
- package/dist/core/connection.d.ts +203 -2
- package/dist/core/connection.js +810 -17
- package/dist/core/conversion.d.ts +153 -0
- package/dist/core/conversion.js +191 -0
- package/dist/core/document.d.ts +188 -0
- package/dist/core/document.js +457 -0
- package/dist/core/edit.d.ts +446 -0
- package/dist/core/edit.js +1994 -0
- package/dist/core/engines.d.ts +64 -0
- package/dist/core/engines.js +74 -0
- package/dist/core/errors.d.ts +164 -2
- package/dist/core/errors.js +209 -3
- package/dist/core/exploration.d.ts +71 -0
- package/dist/core/exploration.js +116 -0
- package/dist/core/graphs.d.ts +15 -0
- package/dist/core/graphs.js +3 -0
- package/dist/core/index.d.ts +30 -9
- package/dist/core/index.js +14 -5
- package/dist/core/model.d.ts +168 -8
- package/dist/core/model.js +258 -31
- package/dist/core/package.d.ts +2 -0
- package/dist/core/package.js +2 -0
- package/dist/core/query.d.ts +50 -0
- package/dist/core/query.js +197 -0
- package/dist/core/render-view.d.ts +97 -0
- package/dist/core/render-view.js +120 -0
- package/dist/core/sources.d.ts +34 -0
- package/dist/core/sources.js +134 -0
- package/dist/core/status.d.ts +9 -2
- package/dist/core/status.js +24 -8
- package/dist/core/typed.d.ts +71 -0
- package/dist/core/typed.js +245 -0
- package/dist/core/values.d.ts +127 -8
- package/dist/core/values.js +536 -30
- package/dist/core/verdict.d.ts +271 -0
- package/dist/core/verdict.js +567 -0
- package/dist/core/wasm.d.ts +114 -0
- package/dist/core/wasm.js +514 -0
- package/dist/generated/sysml_pb.d.ts +1062 -15
- package/dist/generated/sysml_pb.js +200 -100
- package/dist/node/generate-cli.d.ts +2 -0
- package/dist/node/generate-cli.js +10 -0
- package/dist/node/generate.d.ts +99 -0
- package/dist/node/generate.js +757 -0
- package/dist/node/index.d.ts +29 -2
- package/dist/node/index.js +211 -10
- package/dist/node/service.d.ts +5 -3
- package/dist/node/service.js +32 -26
- package/dist/node/wasm-source.d.ts +18 -0
- package/dist/node/wasm-source.js +103 -0
- package/dist/node/wasm-worker.d.ts +1 -0
- package/dist/node/wasm-worker.js +10 -0
- package/dist/node/wasm.d.ts +17 -0
- package/dist/node/wasm.js +63 -0
- package/package.json +19 -7
- package/release-digests.json +105 -0
package/dist/node/index.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Connection } from "../core/connection.js";
|
|
2
|
+
import { Conversion, Migration } from "../core/conversion.js";
|
|
2
3
|
import type { TransportOptions } from "../core/connection.js";
|
|
3
4
|
import { Model } from "../core/model.js";
|
|
4
5
|
import type { ParseOptions } from "../core/model.js";
|
|
5
6
|
export * from "../core/index.js";
|
|
6
7
|
export { ALLOW_UNPINNED_ENV, API_BASE_URL, BINARY_ENV, BinaryNotFoundError, DEFAULT_GITHUB_REPO, NETWORK_TIMEOUT_MS, PINNED_SHA256, RELEASES_BASE_URL, REPO_ENV, VERSION_ENV, binaryName, cachedBinaryPath, cachedRelease, defaultGithubRepo, downloadBinary, expectedDigest, metadataPath, pinnedDigest, platformPackage, releaseAssetName, releaseDownloadUrl, resolveBinary, resolveLatestVersion, signedManifestDigest, staleCacheReason, unpinnedDownloadsAllowed, verifyChecksum, writeMetadata, } from "./binary.js";
|
|
7
|
-
export type { Binary, CacheMetadata, DownloadOptions, PinnedDigests } from "./binary.js";
|
|
8
|
+
export type { Binary, CacheMetadata, DownloadOptions, PinnedDigests, } from "./binary.js";
|
|
8
9
|
export { BUNDLE_ASSET, MANIFEST_ASSET, ReleaseSigner, SIGNED_MANIFEST_SIGNERS, manifestDigest, signerFor, verifiedManifestDigest, verifyManifest, } from "./signing.js";
|
|
9
10
|
export { PrivateService, currentPrivateService } from "./service.js";
|
|
11
|
+
export { connectWasm } from "./wasm.js";
|
|
12
|
+
export type { WasmConnectOptions } from "./wasm.js";
|
|
10
13
|
/** Names a service to connect to instead of starting one. */
|
|
11
14
|
export declare const SERVICE_ENV = "OPENSYSML_SERVICE";
|
|
12
15
|
/** How this process connects. Without an address it starts a private child. */
|
|
@@ -15,6 +18,13 @@ export interface ConnectOptions extends TransportOptions {
|
|
|
15
18
|
httpVersion?: "1.1" | "2";
|
|
16
19
|
/** Wire protocol. Connect by default; gRPC needs HTTP/2 and a service that serves it. */
|
|
17
20
|
protocol?: "connect" | "grpc";
|
|
21
|
+
/**
|
|
22
|
+
* Release tag the service must report, or 'latest'; $OPENSYSML_GRPC_VERSION
|
|
23
|
+
* when omitted. A mismatch refuses the connection with a StaleServiceError.
|
|
24
|
+
*/
|
|
25
|
+
version?: string;
|
|
26
|
+
/** Capabilities the service must report for the connection to be returned. */
|
|
27
|
+
requireCapabilities?: readonly string[];
|
|
18
28
|
}
|
|
19
29
|
/**
|
|
20
30
|
* Connects to a sysml-grpc service.
|
|
@@ -25,7 +35,24 @@ export interface ConnectOptions extends TransportOptions {
|
|
|
25
35
|
* closing the connection leaves that service running.
|
|
26
36
|
*/
|
|
27
37
|
export declare function connect(options?: ConnectOptions): Promise<Connection>;
|
|
28
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Writes `target` to `path`: a model converted in the format its extension
|
|
40
|
+
* names, a conversion's content written as the service returned it, or a
|
|
41
|
+
* migration's content with its image files beside it, at the relative paths
|
|
42
|
+
* the migrated model refers to them with, as `sysml -migrate -o` writes them.
|
|
43
|
+
* A migration that would replace the v1 model it was read from, or land an
|
|
44
|
+
* image outside `path`'s directory, over the model or through a symbolic link
|
|
45
|
+
* at its path, is refused with a `RangeError` before anything is written.
|
|
46
|
+
* Images are then written without following a link at their path, so one put
|
|
47
|
+
* there after the check is not followed; a directory on the way replaced
|
|
48
|
+
* while the write is under way is not guarded against, as `sysml -migrate -o`
|
|
49
|
+
* does not either.
|
|
50
|
+
*/
|
|
51
|
+
export declare function save(target: Migration, path: string): Promise<Migration>;
|
|
52
|
+
export declare function save(target: Model | Conversion, path: string, options?: {
|
|
53
|
+
toFormat?: string;
|
|
54
|
+
tolerateSyntaxErrors?: boolean;
|
|
55
|
+
}): Promise<Conversion>;
|
|
29
56
|
export declare function load(path: string, options?: ConnectOptions & ParseOptions): Promise<Model>;
|
|
30
57
|
/** Parses inline source over a connection of its own, which the model closes. */
|
|
31
58
|
export declare function loads(source: string, options?: ConnectOptions & ParseOptions): Promise<Model>;
|
package/dist/node/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// Node entry point: everything the core does, plus the private-child lifecycle.
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, readlink, realpath, stat, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
5
|
+
import { createConnectTransport, createGrpcTransport, } from "@connectrpc/connect-node";
|
|
3
6
|
import { Connection } from "../core/connection.js";
|
|
7
|
+
import { Conversion, Migration, formatOfPath } from "../core/conversion.js";
|
|
4
8
|
import { OpenSysMLError } from "../core/errors.js";
|
|
5
9
|
import { Model } from "../core/model.js";
|
|
6
|
-
import { baseUrl, encodingOf, interceptors, timeoutOf } from "../core/transport.js";
|
|
10
|
+
import { baseUrl, encodingOf, interceptors, timeoutOf, } from "../core/transport.js";
|
|
11
|
+
import { resolveLatestVersion, VERSION_ENV } from "./binary.js";
|
|
7
12
|
import { acquirePrivateService } from "./service.js";
|
|
8
13
|
export * from "../core/index.js";
|
|
9
14
|
export { ALLOW_UNPINNED_ENV, API_BASE_URL, BINARY_ENV, BinaryNotFoundError, DEFAULT_GITHUB_REPO, NETWORK_TIMEOUT_MS, PINNED_SHA256, RELEASES_BASE_URL, REPO_ENV, VERSION_ENV, binaryName, cachedBinaryPath, cachedRelease, defaultGithubRepo, downloadBinary, expectedDigest, metadataPath, pinnedDigest, platformPackage, releaseAssetName, releaseDownloadUrl, resolveBinary, resolveLatestVersion, signedManifestDigest, staleCacheReason, unpinnedDownloadsAllowed, verifyChecksum, writeMetadata, } from "./binary.js";
|
|
10
15
|
export { BUNDLE_ASSET, MANIFEST_ASSET, ReleaseSigner, SIGNED_MANIFEST_SIGNERS, manifestDigest, signerFor, verifiedManifestDigest, verifyManifest, } from "./signing.js";
|
|
11
16
|
export { PrivateService, currentPrivateService } from "./service.js";
|
|
17
|
+
export { connectWasm } from "./wasm.js";
|
|
12
18
|
/** Names a service to connect to instead of starting one. */
|
|
13
19
|
export const SERVICE_ENV = "OPENSYSML_SERVICE";
|
|
14
20
|
/**
|
|
@@ -26,7 +32,147 @@ export async function connect(options = {}) {
|
|
|
26
32
|
}
|
|
27
33
|
return connectPrivate(options);
|
|
28
34
|
}
|
|
35
|
+
export async function save(target, path, options = {}) {
|
|
36
|
+
if (target instanceof Migration) {
|
|
37
|
+
const files = await migrationFiles(target, path);
|
|
38
|
+
await writeFile(path, target.content, "utf8");
|
|
39
|
+
await files.reduce((previous, [file, data]) => previous.then(() => writeImage(file, data)), Promise.resolve());
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
const conversion = target instanceof Conversion
|
|
43
|
+
? target
|
|
44
|
+
: await target.convert(options.toFormat ?? formatOfPath(path), {
|
|
45
|
+
...(options.tolerateSyntaxErrors === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { tolerateSyntaxErrors: options.tolerateSyntaxErrors }),
|
|
48
|
+
});
|
|
49
|
+
await writeFile(path, conversion.content, "utf8");
|
|
50
|
+
return conversion;
|
|
51
|
+
}
|
|
29
52
|
/** Parses a file over a connection of its own, which the model closes. */
|
|
53
|
+
/**
|
|
54
|
+
* Where a migration's image files land when its model is written to `path`,
|
|
55
|
+
* refusing any that would escape the model's directory or replace the model
|
|
56
|
+
* or the v1 source, and refusing `path` itself when it is the source. A source
|
|
57
|
+
* no longer at its path protects nothing: nothing of it would be replaced.
|
|
58
|
+
*/
|
|
59
|
+
async function migrationFiles(migration, path) {
|
|
60
|
+
const source = (await exists(migration.sourcePath))
|
|
61
|
+
? migration.sourcePath
|
|
62
|
+
: "";
|
|
63
|
+
if (source !== "" && (await samePath(path, source))) {
|
|
64
|
+
throw new RangeError(`${path} names the model being migrated; the v1 model would be replaced by its migration`);
|
|
65
|
+
}
|
|
66
|
+
const base = resolve(dirname(path));
|
|
67
|
+
const landedBase = await landing(base);
|
|
68
|
+
const guarded = [path, source].filter((candidate) => candidate !== "");
|
|
69
|
+
return Promise.all([...migration.files].map(async ([name, data]) => {
|
|
70
|
+
const file = await imageFile(name, base, landedBase, guarded);
|
|
71
|
+
return [file, data];
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Where the image `name` lands under `base`, refusing one that would escape
|
|
76
|
+
* `base`, replace a guarded path, or be written through a symbolic link.
|
|
77
|
+
*/
|
|
78
|
+
async function imageFile(name, base, landedBase, guarded) {
|
|
79
|
+
const segments = name.split("/");
|
|
80
|
+
const file = resolve(base, ...segments);
|
|
81
|
+
if (segments.some((segment) => segment === "" ||
|
|
82
|
+
segment === "." ||
|
|
83
|
+
segment === ".." ||
|
|
84
|
+
segment.includes("\\")) ||
|
|
85
|
+
!within(base, file) ||
|
|
86
|
+
!within(landedBase, await landing(file))) {
|
|
87
|
+
throw new RangeError(`the migration's image ${name} would land outside ${base}`);
|
|
88
|
+
}
|
|
89
|
+
const replaced = await Promise.all(guarded.map((candidate) => samePath(file, candidate)));
|
|
90
|
+
const replacing = guarded.find((_, index) => replaced[index]);
|
|
91
|
+
if (replacing !== undefined) {
|
|
92
|
+
throw new RangeError(`the migration's image ${name} would replace ${replacing}`);
|
|
93
|
+
}
|
|
94
|
+
if (await isSymbolicLink(file)) {
|
|
95
|
+
throw new RangeError(`the migration's image ${name} would be written through a symbolic link at ${file}`);
|
|
96
|
+
}
|
|
97
|
+
return file;
|
|
98
|
+
}
|
|
99
|
+
/** Writes one image, creating its directory. */
|
|
100
|
+
async function writeImage(file, data) {
|
|
101
|
+
await mkdir(dirname(file), { recursive: true });
|
|
102
|
+
await writeFile(file, data, { flag: IMAGE_FLAGS });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* How an image is written: created or truncated in place, never through a link
|
|
106
|
+
* at its path. Windows has no O_NOFOLLOW; the bitwise or reads its absence as 0.
|
|
107
|
+
*/
|
|
108
|
+
const IMAGE_FLAGS = constants.O_WRONLY |
|
|
109
|
+
constants.O_CREAT |
|
|
110
|
+
constants.O_TRUNC |
|
|
111
|
+
constants.O_NOFOLLOW;
|
|
112
|
+
/** Whether `path` is itself a symbolic link, wherever it points. */
|
|
113
|
+
function isSymbolicLink(path) {
|
|
114
|
+
return lstat(path).then((status) => status.isSymbolicLink(), () => false);
|
|
115
|
+
}
|
|
116
|
+
/** Whether `path` names something: an empty path, or one that is gone, does not. */
|
|
117
|
+
async function exists(path) {
|
|
118
|
+
return (path !== "" &&
|
|
119
|
+
(await stat(path).then(() => true, () => false)));
|
|
120
|
+
}
|
|
121
|
+
/** Whether `file` lies strictly below the directory `base`, both absolute. */
|
|
122
|
+
function within(base, file) {
|
|
123
|
+
const below = relative(base, file);
|
|
124
|
+
return (below !== "" &&
|
|
125
|
+
!isAbsolute(below) &&
|
|
126
|
+
below !== ".." &&
|
|
127
|
+
!below.startsWith(".." + sep));
|
|
128
|
+
}
|
|
129
|
+
/** Whether a write to `a` lands on `b`: the same file when both exist, else the same resolved path. */
|
|
130
|
+
async function samePath(a, b) {
|
|
131
|
+
const [statA, statB] = await Promise.all([
|
|
132
|
+
stat(a, { bigint: true }).catch(() => undefined),
|
|
133
|
+
stat(b, { bigint: true }).catch(() => undefined),
|
|
134
|
+
]);
|
|
135
|
+
if (statA !== undefined &&
|
|
136
|
+
statB !== undefined &&
|
|
137
|
+
statA.ino !== 0n &&
|
|
138
|
+
statB.ino !== 0n) {
|
|
139
|
+
return statA.dev === statB.dev && statA.ino === statB.ino;
|
|
140
|
+
}
|
|
141
|
+
return (await landing(a)) === (await landing(b));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The absolute path a write to `path` lands on: every symbolic link on the way
|
|
145
|
+
* followed, a dangling one included, through the deepest ancestor that exists,
|
|
146
|
+
* whatever below it does not.
|
|
147
|
+
*/
|
|
148
|
+
async function landing(path) {
|
|
149
|
+
let head = resolve(path);
|
|
150
|
+
const tail = [];
|
|
151
|
+
let hops = 0;
|
|
152
|
+
for (;;) {
|
|
153
|
+
try {
|
|
154
|
+
return join(await realpath(head), ...tail);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
const target = await readlink(head).catch(() => undefined);
|
|
158
|
+
if (target !== undefined) {
|
|
159
|
+
if (++hops >= MAX_SYMLINK_HOPS) {
|
|
160
|
+
throw new RangeError(`${path}: too many levels of symbolic links`);
|
|
161
|
+
}
|
|
162
|
+
head = resolve(dirname(head), target);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const parent = dirname(head);
|
|
166
|
+
if (parent === head) {
|
|
167
|
+
return join(head, ...tail);
|
|
168
|
+
}
|
|
169
|
+
tail.unshift(basename(head));
|
|
170
|
+
head = parent;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Dangling symbolic links followed before a path is judged to loop, as the kernel's limit. */
|
|
175
|
+
const MAX_SYMLINK_HOPS = 40;
|
|
30
176
|
export async function load(path, options = {}) {
|
|
31
177
|
const connection = await connect(options);
|
|
32
178
|
try {
|
|
@@ -48,31 +194,69 @@ export async function loads(source, options = {}) {
|
|
|
48
194
|
throw error;
|
|
49
195
|
}
|
|
50
196
|
}
|
|
197
|
+
// The release a connection requires, 'latest' resolved to the tag it names.
|
|
198
|
+
async function requiredRelease(options) {
|
|
199
|
+
const asked = options.version ?? process.env[VERSION_ENV];
|
|
200
|
+
if (asked === undefined || asked === "") {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
if (asked === "latest") {
|
|
204
|
+
try {
|
|
205
|
+
return await resolveLatestVersion();
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// A release that cannot be resolved is not required, as ensure_binary reads it.
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return asked;
|
|
213
|
+
}
|
|
51
214
|
async function connectPrivate(options) {
|
|
52
215
|
// Checked before a child is started, so a bad option costs no process.
|
|
53
216
|
const encoding = encodingOf(options);
|
|
54
217
|
const timeoutMs = timeoutOf(options);
|
|
55
|
-
const
|
|
218
|
+
const required = await requiredRelease(options);
|
|
219
|
+
const service = await acquirePrivateService(required);
|
|
56
220
|
const backend = {
|
|
57
221
|
origin: `${service.binary.path}, started by this client`,
|
|
58
222
|
release: () => service.release(),
|
|
223
|
+
warn: (message, type) => {
|
|
224
|
+
process.emitWarning(message, type);
|
|
225
|
+
},
|
|
226
|
+
resolvePath: (path) => resolve(path),
|
|
59
227
|
};
|
|
228
|
+
// The open owns release from here on: a failure inside Connection.open
|
|
229
|
+
// releases exactly once, so a refusal cannot drop another connection's hold.
|
|
230
|
+
let transport;
|
|
60
231
|
try {
|
|
61
|
-
|
|
62
|
-
transport: transportFor(baseUrl(service.address), options),
|
|
63
|
-
encoding,
|
|
64
|
-
backend,
|
|
65
|
-
timeoutMs,
|
|
66
|
-
});
|
|
232
|
+
transport = transportFor(baseUrl(service.address), options);
|
|
67
233
|
}
|
|
68
234
|
catch (error) {
|
|
69
235
|
await service.release();
|
|
70
236
|
throw error;
|
|
71
237
|
}
|
|
238
|
+
return Connection.open({
|
|
239
|
+
transport,
|
|
240
|
+
encoding,
|
|
241
|
+
backend,
|
|
242
|
+
timeoutMs,
|
|
243
|
+
...(required === undefined ? {} : { requiredVersion: required }),
|
|
244
|
+
...(options.requireCapabilities === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: { requiredCapabilities: options.requireCapabilities }),
|
|
247
|
+
stale: {
|
|
248
|
+
address: service.address,
|
|
249
|
+
remedy: `the binary this client started, ${service.binary.path}, is not ` +
|
|
250
|
+
`${required ?? "the release asked for"}: make that release available (its ` +
|
|
251
|
+
`download is cached under ~/.opensysml/bin), or accept what is installed ` +
|
|
252
|
+
`by passing version: undefined and unsetting $${VERSION_ENV}`,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
72
255
|
}
|
|
73
256
|
async function connectExternal(address, options) {
|
|
74
257
|
const url = baseUrl(address);
|
|
75
258
|
const timeoutMs = timeoutOf(options);
|
|
259
|
+
const required = await requiredRelease(options);
|
|
76
260
|
return Connection.open({
|
|
77
261
|
transport: transportFor(url, options),
|
|
78
262
|
encoding: encodingOf(options),
|
|
@@ -80,8 +264,22 @@ async function connectExternal(address, options) {
|
|
|
80
264
|
origin: `${url}, which this client did not start`,
|
|
81
265
|
// A service this client did not start is never stopped by it.
|
|
82
266
|
release: () => Promise.resolve(),
|
|
267
|
+
warn: (message, type) => {
|
|
268
|
+
process.emitWarning(message, type);
|
|
269
|
+
},
|
|
270
|
+
resolvePath: (path) => resolve(path),
|
|
83
271
|
},
|
|
84
272
|
timeoutMs,
|
|
273
|
+
...(required === undefined ? {} : { requiredVersion: required }),
|
|
274
|
+
...(options.requireCapabilities === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: { requiredCapabilities: options.requireCapabilities }),
|
|
277
|
+
stale: {
|
|
278
|
+
address: url,
|
|
279
|
+
remedy: `stop the service listening on ${url} yourself and let this client start ` +
|
|
280
|
+
`a ${required ?? "matching"} one, or accept what is running by passing ` +
|
|
281
|
+
`version: undefined and unsetting $${VERSION_ENV}`,
|
|
282
|
+
},
|
|
85
283
|
});
|
|
86
284
|
}
|
|
87
285
|
function transportFor(url, options) {
|
|
@@ -90,7 +288,10 @@ function transportFor(url, options) {
|
|
|
90
288
|
if (!binary) {
|
|
91
289
|
throw new OpenSysMLError("the gRPC protocol carries protobuf bodies; JSON needs the Connect protocol");
|
|
92
290
|
}
|
|
93
|
-
return createGrpcTransport({
|
|
291
|
+
return createGrpcTransport({
|
|
292
|
+
baseUrl: url,
|
|
293
|
+
interceptors: interceptors(options),
|
|
294
|
+
});
|
|
94
295
|
}
|
|
95
296
|
return createConnectTransport({
|
|
96
297
|
baseUrl: url,
|
package/dist/node/service.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare class PrivateService {
|
|
|
8
8
|
private readonly child;
|
|
9
9
|
private readonly stderr;
|
|
10
10
|
private ended;
|
|
11
|
+
/** The release key {@link acquirePrivateService} filed this child under. */
|
|
12
|
+
key: string;
|
|
11
13
|
private constructor();
|
|
12
14
|
/** The process id of the child, which the orphan-safety test reads. */
|
|
13
15
|
get pid(): number | undefined;
|
|
@@ -23,9 +25,9 @@ export declare class PrivateService {
|
|
|
23
25
|
*/
|
|
24
26
|
stop(): Promise<void>;
|
|
25
27
|
/** Starts a child and waits for the address it bound. */
|
|
26
|
-
static start(): Promise<PrivateService>;
|
|
28
|
+
static start(version?: string): Promise<PrivateService>;
|
|
27
29
|
}
|
|
28
|
-
/** Takes a hold on
|
|
29
|
-
export declare function acquirePrivateService(): Promise<PrivateService>;
|
|
30
|
+
/** Takes a hold on the private child for `version`, starting one when there is none. */
|
|
31
|
+
export declare function acquirePrivateService(version?: string): Promise<PrivateService>;
|
|
30
32
|
/** The private child this thread holds, for tests and diagnostics. */
|
|
31
33
|
export declare function currentPrivateService(): PrivateService | undefined;
|
package/dist/node/service.js
CHANGED
|
@@ -7,10 +7,10 @@ import { resolveBinary } from "./binary.js";
|
|
|
7
7
|
const START_TIMEOUT_MS = 10_000;
|
|
8
8
|
const STOP_TIMEOUT_MS = 5_000;
|
|
9
9
|
const STDERR_LINES_KEPT = 20;
|
|
10
|
-
/** The
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
10
|
+
/** The private children of this thread, one per release asked for, keyed by tag. */
|
|
11
|
+
const shared = new Map();
|
|
12
|
+
/** Starts in flight, so concurrent connect() calls share one child per release. */
|
|
13
|
+
const starting = new Map();
|
|
14
14
|
/** A sysml-grpc child of this process, shared by every connection that needs one. */
|
|
15
15
|
export class PrivateService {
|
|
16
16
|
address;
|
|
@@ -20,6 +20,8 @@ export class PrivateService {
|
|
|
20
20
|
child;
|
|
21
21
|
stderr;
|
|
22
22
|
ended;
|
|
23
|
+
/** The release key {@link acquirePrivateService} filed this child under. */
|
|
24
|
+
key = "";
|
|
23
25
|
constructor(init) {
|
|
24
26
|
this.child = init.child;
|
|
25
27
|
this.binary = init.binary;
|
|
@@ -48,8 +50,8 @@ export class PrivateService {
|
|
|
48
50
|
if (this.refs > 0) {
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
|
-
if (shared === this) {
|
|
52
|
-
shared
|
|
53
|
+
if (shared.get(this.key) === this) {
|
|
54
|
+
shared.delete(this.key);
|
|
53
55
|
}
|
|
54
56
|
await this.stop();
|
|
55
57
|
}
|
|
@@ -82,8 +84,8 @@ export class PrivateService {
|
|
|
82
84
|
this.child.stdin.destroy();
|
|
83
85
|
}
|
|
84
86
|
/** Starts a child and waits for the address it bound. */
|
|
85
|
-
static async start() {
|
|
86
|
-
const binary = await resolveBinary();
|
|
87
|
+
static async start(version) {
|
|
88
|
+
const binary = await resolveBinary(version === undefined ? {} : { version });
|
|
87
89
|
const child = spawn(binary.path, ["-port", "0", "-health-port", "0", "-report-address", "-exit-with-parent"], {
|
|
88
90
|
stdio: ["pipe", "pipe", "pipe"],
|
|
89
91
|
// A new session (a new process group on Windows), so a Ctrl-C meant for
|
|
@@ -118,28 +120,32 @@ export class PrivateService {
|
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
|
-
/** Takes a hold on
|
|
122
|
-
export async function acquirePrivateService() {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
starting ??= PrivateService.start()
|
|
130
|
-
.then((service) => {
|
|
131
|
-
shared = service;
|
|
132
|
-
return service;
|
|
133
|
-
})
|
|
134
|
-
.finally(() => {
|
|
135
|
-
starting = undefined;
|
|
136
|
-
});
|
|
137
|
-
await starting;
|
|
123
|
+
/** Takes a hold on the private child for `version`, starting one when there is none. */
|
|
124
|
+
export async function acquirePrivateService(version) {
|
|
125
|
+
const key = version ?? "";
|
|
126
|
+
const existing = shared.get(key);
|
|
127
|
+
if (existing?.alive) {
|
|
128
|
+
existing.refs += 1;
|
|
129
|
+
return existing;
|
|
138
130
|
}
|
|
131
|
+
let pending = starting.get(key);
|
|
132
|
+
pending ??= PrivateService.start(version)
|
|
133
|
+
.then((service) => {
|
|
134
|
+
service.key = key;
|
|
135
|
+
shared.set(key, service);
|
|
136
|
+
return service;
|
|
137
|
+
})
|
|
138
|
+
.finally(() => {
|
|
139
|
+
starting.delete(key);
|
|
140
|
+
});
|
|
141
|
+
starting.set(key, pending);
|
|
142
|
+
await pending;
|
|
143
|
+
// The child just started is taken by the same path as one found alive.
|
|
144
|
+
return acquirePrivateService(version);
|
|
139
145
|
}
|
|
140
146
|
/** The private child this thread holds, for tests and diagnostics. */
|
|
141
147
|
export function currentPrivateService() {
|
|
142
|
-
return shared;
|
|
148
|
+
return [...shared.values()].find((service) => service.refs > 0) ?? [...shared.values()][0];
|
|
143
149
|
}
|
|
144
150
|
function startFailure(binary, stderr, cause) {
|
|
145
151
|
const why = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { WorkerWasmSource } from "../core/wasm.js";
|
|
2
|
+
export interface NodeWasmSourceOptions {
|
|
3
|
+
wasm?: string | URL | Uint8Array;
|
|
4
|
+
wasmExec?: string | URL;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolvedNodeWasmSources {
|
|
7
|
+
wasm: string | URL | Uint8Array;
|
|
8
|
+
wasmExec: string | URL;
|
|
9
|
+
}
|
|
10
|
+
export type PackageJsonResolver = (specifier: string) => string;
|
|
11
|
+
export declare function loadNodeWasm(source: WorkerWasmSource | URL): Promise<BufferSource | WebAssembly.Module>;
|
|
12
|
+
export declare function resolveWasmSources(options: NodeWasmSourceOptions, resolvePackageJson?: PackageJsonResolver): ResolvedNodeWasmSources;
|
|
13
|
+
export declare function resolveWasmPackage(resolvePackageJson?: PackageJsonResolver): {
|
|
14
|
+
wasm: string;
|
|
15
|
+
wasmExec: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function moduleSpecifier(source: string | URL): string;
|
|
18
|
+
export declare function parseUrl(source: string): URL | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { OpenSysMLError } from "../core/errors.js";
|
|
7
|
+
import { PACKAGE_NAME, WASM_PACKAGE } from "../core/package.js";
|
|
8
|
+
export async function loadNodeWasm(source) {
|
|
9
|
+
if (source instanceof WebAssembly.Module ||
|
|
10
|
+
source instanceof ArrayBuffer ||
|
|
11
|
+
ArrayBuffer.isView(source)) {
|
|
12
|
+
return source;
|
|
13
|
+
}
|
|
14
|
+
const url = source instanceof URL ? source : parseUrl(source);
|
|
15
|
+
if (url?.protocol === "file:") {
|
|
16
|
+
return readFile(url);
|
|
17
|
+
}
|
|
18
|
+
if (url !== undefined) {
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`could not fetch the WebAssembly module: ${response.status}`);
|
|
22
|
+
}
|
|
23
|
+
return response.arrayBuffer();
|
|
24
|
+
}
|
|
25
|
+
if (typeof source === "string") {
|
|
26
|
+
return readFile(isAbsolute(source) ? source : resolve(source));
|
|
27
|
+
}
|
|
28
|
+
return readFile(source);
|
|
29
|
+
}
|
|
30
|
+
export function resolveWasmSources(options, resolvePackageJson = createRequire(import.meta.url).resolve) {
|
|
31
|
+
if (options.wasm !== undefined) {
|
|
32
|
+
if (options.wasmExec === undefined) {
|
|
33
|
+
throw new OpenSysMLError("connectWasm needs the wasmExec option when wasm is provided");
|
|
34
|
+
}
|
|
35
|
+
return { wasm: options.wasm, wasmExec: options.wasmExec };
|
|
36
|
+
}
|
|
37
|
+
const assets = resolveWasmPackage(resolvePackageJson);
|
|
38
|
+
return {
|
|
39
|
+
wasm: assets.wasm,
|
|
40
|
+
wasmExec: options.wasmExec ?? assets.wasmExec,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function resolveWasmPackage(resolvePackageJson = createRequire(import.meta.url).resolve) {
|
|
44
|
+
let packageJson;
|
|
45
|
+
try {
|
|
46
|
+
packageJson = resolvePackageJson(`${WASM_PACKAGE}/package.json`);
|
|
47
|
+
}
|
|
48
|
+
catch (cause) {
|
|
49
|
+
throw new OpenSysMLError(`connectWasm needs sysml-wasm.wasm: install ${WASM_PACKAGE} at ${clientVersion()}, or pass wasm and wasmExec`, { cause });
|
|
50
|
+
}
|
|
51
|
+
const packageDirectory = dirname(packageJson);
|
|
52
|
+
return {
|
|
53
|
+
wasm: join(packageDirectory, "sysml-wasm.wasm"),
|
|
54
|
+
wasmExec: join(packageDirectory, "wasm_exec.js"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function moduleSpecifier(source) {
|
|
58
|
+
if (source instanceof URL) {
|
|
59
|
+
return source.href;
|
|
60
|
+
}
|
|
61
|
+
if (!/^[A-Za-z]:[\\/]/.test(source)) {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(source).href;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Resolve filesystem paths relative to the current working directory.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return pathToFileURL(isAbsolute(source) ? source : resolve(source)).href;
|
|
70
|
+
}
|
|
71
|
+
export function parseUrl(source) {
|
|
72
|
+
if (/^[A-Za-z]:[\\/]/.test(source)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return new URL(source);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function clientVersion() {
|
|
83
|
+
let directory = dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
for (;;) {
|
|
85
|
+
const packageJson = join(directory, "package.json");
|
|
86
|
+
if (existsSync(packageJson)) {
|
|
87
|
+
const metadata = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
88
|
+
if (typeof metadata === "object" &&
|
|
89
|
+
metadata !== null &&
|
|
90
|
+
"name" in metadata &&
|
|
91
|
+
metadata.name === PACKAGE_NAME &&
|
|
92
|
+
"version" in metadata &&
|
|
93
|
+
typeof metadata.version === "string") {
|
|
94
|
+
return metadata.version;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const parent = dirname(directory);
|
|
98
|
+
if (parent === directory) {
|
|
99
|
+
throw new OpenSysMLError("the OpenSysML Node package version could not be found");
|
|
100
|
+
}
|
|
101
|
+
directory = parent;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { loadGoConstructor, serveWasmPort, } from "../core/wasm.js";
|
|
3
|
+
import { loadNodeWasm, moduleSpecifier } from "./wasm-source.js";
|
|
4
|
+
if (parentPort === null) {
|
|
5
|
+
throw new Error("the sysml-wasm worker must run in a worker thread");
|
|
6
|
+
}
|
|
7
|
+
serveWasmPort(parentPort, {
|
|
8
|
+
loadWasm: loadNodeWasm,
|
|
9
|
+
loadGo: (wasmExec) => loadGoConstructor(wasmExec === undefined ? undefined : moduleSpecifier(wasmExec)),
|
|
10
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Connection } from "../core/connection.js";
|
|
2
|
+
import type { TransportOptions } from "../core/connection.js";
|
|
3
|
+
/** Options for connecting to a Go WebAssembly module from Node. */
|
|
4
|
+
export interface WasmConnectOptions extends TransportOptions {
|
|
5
|
+
/** Path, file URL, HTTP URL, or bytes of sysml-wasm.wasm. */
|
|
6
|
+
wasm?: string | URL | Uint8Array;
|
|
7
|
+
/** wasm_exec.js from the Go toolchain that built the module. */
|
|
8
|
+
wasmExec?: string | URL;
|
|
9
|
+
/** Run the module in a worker thread (default) or this thread. */
|
|
10
|
+
thread?: "worker" | "inline";
|
|
11
|
+
version?: string;
|
|
12
|
+
requireCapabilities?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Connects to sysml-wasm, in a worker by default or inline when requested.
|
|
16
|
+
*/
|
|
17
|
+
export declare function connectWasm(options?: WasmConnectOptions): Promise<Connection>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { Connection } from "../core/connection.js";
|
|
3
|
+
import { connectWasmHost, instantiateInline, loadGoConstructor, WorkerWasmHost, } from "../core/wasm.js";
|
|
4
|
+
import { loadNodeWasm, moduleSpecifier, resolveWasmSources } from "./wasm-source.js";
|
|
5
|
+
/**
|
|
6
|
+
* Connects to sysml-wasm, in a worker by default or inline when requested.
|
|
7
|
+
*/
|
|
8
|
+
export async function connectWasm(options = {}) {
|
|
9
|
+
const sources = resolveWasmSources(options);
|
|
10
|
+
const wasmExec = moduleSpecifier(sources.wasmExec);
|
|
11
|
+
let host;
|
|
12
|
+
if (options.thread === "inline") {
|
|
13
|
+
const module = await loadNodeWasm(sources.wasm);
|
|
14
|
+
const Go = await loadGoConstructor(wasmExec);
|
|
15
|
+
host = await instantiateInline(module, Go);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
host = await startWorker(sources.wasm, wasmExec);
|
|
19
|
+
}
|
|
20
|
+
return connectWasmHost(host, options);
|
|
21
|
+
}
|
|
22
|
+
async function startWorker(wasm, wasmExec) {
|
|
23
|
+
const worker = new Worker(new URL("./wasm-worker.js", import.meta.url));
|
|
24
|
+
const host = new WorkerWasmHost(nodeWorkerEndpoint(worker));
|
|
25
|
+
const { source, transfer } = workerSource(wasm);
|
|
26
|
+
try {
|
|
27
|
+
return await host.start({ type: "init", wasm: source, wasmExec }, transfer);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
await host.close();
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function workerSource(wasm) {
|
|
35
|
+
if (wasm instanceof Uint8Array) {
|
|
36
|
+
const bytes = Uint8Array.from(wasm);
|
|
37
|
+
return { source: bytes.buffer, transfer: [bytes.buffer] };
|
|
38
|
+
}
|
|
39
|
+
return { source: wasm instanceof URL ? wasm.href : wasm, transfer: [] };
|
|
40
|
+
}
|
|
41
|
+
function nodeWorkerEndpoint(worker) {
|
|
42
|
+
let closed = false;
|
|
43
|
+
return {
|
|
44
|
+
post(message, transfer) {
|
|
45
|
+
worker.postMessage(message, [...transfer]);
|
|
46
|
+
},
|
|
47
|
+
onMessage(listener) {
|
|
48
|
+
worker.on("message", listener);
|
|
49
|
+
},
|
|
50
|
+
onFailure(listener) {
|
|
51
|
+
worker.on("error", listener);
|
|
52
|
+
worker.on("exit", (code) => {
|
|
53
|
+
if (!closed) {
|
|
54
|
+
listener(new Error(`the sysml-wasm worker exited with code ${code}`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
async terminate() {
|
|
59
|
+
closed = true;
|
|
60
|
+
await worker.terminate();
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|