miravo 0.6.0 → 0.6.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/LICENSE +199 -0
- package/README.md +7 -70
- package/bin/miravo.js +84 -0
- package/package.json +20 -36
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared CLI helpers for LE apply/cleanup commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { basename } from "node:path";
|
|
6
|
-
import { TEMPLATE_FILE_EXTENSION } from "@miravo/shared";
|
|
7
|
-
import type { ApplyPlan, ApplyScope } from "./le-plan.ts";
|
|
8
|
-
import { renderPlanHuman, renderPlanJSON } from "./le-plan.ts";
|
|
9
|
-
|
|
10
|
-
const VALID_SCOPE_VALUES = new Set<ApplyScope>(["devices", "models", "instances"]);
|
|
11
|
-
|
|
12
|
-
export function parseScopeOption(
|
|
13
|
-
rawScope: string | undefined,
|
|
14
|
-
defaultScope: readonly ApplyScope[],
|
|
15
|
-
): { readonly scope?: ApplyScope[]; readonly error?: string } {
|
|
16
|
-
if (!rawScope) {
|
|
17
|
-
return { scope: [...defaultScope] };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const requestedScope = rawScope
|
|
21
|
-
.split(",")
|
|
22
|
-
.map((v) => v.trim())
|
|
23
|
-
.filter((v) => v.length > 0);
|
|
24
|
-
|
|
25
|
-
if (requestedScope.length === 0) {
|
|
26
|
-
return { error: "--scope cannot be empty. Use a comma-separated list." };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const invalidScope = requestedScope.filter((v) => !VALID_SCOPE_VALUES.has(v as ApplyScope));
|
|
30
|
-
if (invalidScope.length > 0) {
|
|
31
|
-
return {
|
|
32
|
-
error: `invalid --scope value(s): ${invalidScope.join(", ")}. Valid values: ${[...defaultScope].join(", ")}.`,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const dedupedScope: ApplyScope[] = [];
|
|
37
|
-
for (const value of requestedScope as ApplyScope[]) {
|
|
38
|
-
if (!dedupedScope.includes(value)) {
|
|
39
|
-
dedupedScope.push(value);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return { scope: dedupedScope };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Extract the template ID (filename stem) from a resolved template path. */
|
|
46
|
-
export function extractTemplateId(templatePath: string): string {
|
|
47
|
-
const fileName = basename(templatePath);
|
|
48
|
-
return fileName.endsWith(TEMPLATE_FILE_EXTENSION)
|
|
49
|
-
? fileName.slice(0, -TEMPLATE_FILE_EXTENSION.length)
|
|
50
|
-
: fileName;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Write status message to stderr (JSON mode) or stdout via writeOutput. */
|
|
54
|
-
export function writeStatusMessage(
|
|
55
|
-
json: boolean,
|
|
56
|
-
message: string,
|
|
57
|
-
writeOutput: (msg: string) => void,
|
|
58
|
-
): void {
|
|
59
|
-
if (json) {
|
|
60
|
-
process.stderr.write(`${message}\n`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
writeOutput(message);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Write a plan to stdout in JSON or human format. */
|
|
67
|
-
export function writePlanOutput(
|
|
68
|
-
plan: ApplyPlan,
|
|
69
|
-
json: boolean,
|
|
70
|
-
writeOutput: (msg: string) => void,
|
|
71
|
-
): void {
|
|
72
|
-
if (json) {
|
|
73
|
-
process.stdout.write(`${renderPlanJSON(plan)}\n`);
|
|
74
|
-
} else {
|
|
75
|
-
writeOutput(renderPlanHuman(plan));
|
|
76
|
-
}
|
|
77
|
-
}
|
package/src/lib/le/le-client.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Litmus Edge GraphQL client.
|
|
3
|
-
*
|
|
4
|
-
* Handles HTTP Basic Auth (API key as username, empty password),
|
|
5
|
-
* TLS skip-verify, and 30-second per-call timeout.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { MiravoError } from "@miravo/shared";
|
|
9
|
-
|
|
10
|
-
const LE_TIMEOUT_MS = 30_000;
|
|
11
|
-
|
|
12
|
-
export interface LEClientConfig {
|
|
13
|
-
readonly hostname: string;
|
|
14
|
-
readonly apiToken: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface GQLResponse<T = unknown> {
|
|
18
|
-
readonly data?: T;
|
|
19
|
-
readonly errors?: readonly { message: string }[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function buildLEClientFix(endpoint: string, statusCode?: number): string {
|
|
23
|
-
if (statusCode === 401 || statusCode === 403) {
|
|
24
|
-
return (
|
|
25
|
-
"Check --le-api-token (or LITMUS_EDGE_API_TOKEN) and verify the token has permission " +
|
|
26
|
-
`for ${endpoint}.`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
if (statusCode === 404) {
|
|
30
|
-
return `LE endpoint "${endpoint}" was not found. Check your Litmus Edge version and hostname.`;
|
|
31
|
-
}
|
|
32
|
-
return (
|
|
33
|
-
"Check --le-hostname / --le-api-token and ensure Litmus Edge is reachable over HTTPS " +
|
|
34
|
-
"from this machine."
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
class LEClientError extends MiravoError {
|
|
39
|
-
constructor(
|
|
40
|
-
message: string,
|
|
41
|
-
fix: string,
|
|
42
|
-
readonly endpoint: string,
|
|
43
|
-
readonly statusCode?: number,
|
|
44
|
-
) {
|
|
45
|
-
super(message, fix);
|
|
46
|
-
this.name = "LEClientError";
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class LEClient {
|
|
51
|
-
private readonly baseUrl: string;
|
|
52
|
-
private readonly authHeader: string;
|
|
53
|
-
|
|
54
|
-
constructor(config: LEClientConfig) {
|
|
55
|
-
const host = config.hostname.includes("://") ? config.hostname : `https://${config.hostname}`;
|
|
56
|
-
this.baseUrl = host.replace(/\/$/, "");
|
|
57
|
-
this.authHeader = `Basic ${btoa(`${config.apiToken}:`)}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async gql<T = unknown>(
|
|
61
|
-
endpoint: string,
|
|
62
|
-
query: string,
|
|
63
|
-
variables?: Record<string, unknown>,
|
|
64
|
-
): Promise<T> {
|
|
65
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
66
|
-
const controller = new AbortController();
|
|
67
|
-
const timeout = setTimeout(() => controller.abort(), LE_TIMEOUT_MS);
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetch(url, {
|
|
71
|
-
method: "POST",
|
|
72
|
-
headers: {
|
|
73
|
-
"Content-Type": "application/json",
|
|
74
|
-
Authorization: this.authHeader,
|
|
75
|
-
},
|
|
76
|
-
body: JSON.stringify({ query, variables }),
|
|
77
|
-
signal: controller.signal,
|
|
78
|
-
tls: { rejectUnauthorized: false },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (!res.ok) {
|
|
82
|
-
const body = await res.text().catch(() => "");
|
|
83
|
-
throw new LEClientError(
|
|
84
|
-
`LE ${endpoint} returned ${res.status}: ${body}`,
|
|
85
|
-
buildLEClientFix(endpoint, res.status),
|
|
86
|
-
endpoint,
|
|
87
|
-
res.status,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const json = (await res.json()) as GQLResponse<T>;
|
|
92
|
-
if (json.errors?.length) {
|
|
93
|
-
throw new LEClientError(
|
|
94
|
-
`LE GraphQL error from ${endpoint}: ${json.errors.map((e) => e.message).join("; ")}`,
|
|
95
|
-
buildLEClientFix(endpoint),
|
|
96
|
-
endpoint,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
if (!json.data) {
|
|
100
|
-
throw new LEClientError(
|
|
101
|
-
`LE ${endpoint} returned empty data`,
|
|
102
|
-
buildLEClientFix(endpoint),
|
|
103
|
-
endpoint,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return json.data;
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (err instanceof LEClientError) throw err;
|
|
109
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
110
|
-
throw new LEClientError(
|
|
111
|
-
`LE ${endpoint} timed out after ${LE_TIMEOUT_MS}ms`,
|
|
112
|
-
`Check network connectivity to ${url} and verify Litmus Edge is healthy.`,
|
|
113
|
-
endpoint,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
throw new LEClientError(
|
|
117
|
-
`LE ${endpoint} request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
118
|
-
buildLEClientFix(endpoint),
|
|
119
|
-
endpoint,
|
|
120
|
-
);
|
|
121
|
-
} finally {
|
|
122
|
-
clearTimeout(timeout);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Verifies credentials via Digital Twins GraphQL (`Healthz`). Prefer `checkSysinfoVersion` for lightweight connectivity checks. */
|
|
127
|
-
async checkAuth(): Promise<void> {
|
|
128
|
-
await this.gql<{ Healthz: boolean }>("/digital-twins/v2", "query { Healthz }");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Verifies reachability + auth via `GET /sysinfo/version` (lighter than `checkAuth`). */
|
|
132
|
-
async checkSysinfoVersion(): Promise<void> {
|
|
133
|
-
const path = "/sysinfo/version";
|
|
134
|
-
const url = `${this.baseUrl}${path}`;
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const timeout = setTimeout(() => controller.abort(), LE_TIMEOUT_MS);
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const res = await fetch(url, {
|
|
140
|
-
method: "GET",
|
|
141
|
-
headers: { Authorization: this.authHeader },
|
|
142
|
-
signal: controller.signal,
|
|
143
|
-
tls: { rejectUnauthorized: false },
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (!res.ok) {
|
|
147
|
-
const body = await res.text().catch(() => "");
|
|
148
|
-
throw new LEClientError(
|
|
149
|
-
`LE ${path} returned ${res.status}: ${body}`,
|
|
150
|
-
buildLEClientFix(path, res.status),
|
|
151
|
-
path,
|
|
152
|
-
res.status,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const data: unknown = await res.json().catch(() => {
|
|
157
|
-
throw new LEClientError(`LE ${path} returned non-JSON body`, buildLEClientFix(path), path);
|
|
158
|
-
});
|
|
159
|
-
if (data === null || typeof data !== "object") {
|
|
160
|
-
throw new LEClientError(
|
|
161
|
-
`LE ${path} returned unexpected JSON`,
|
|
162
|
-
buildLEClientFix(path),
|
|
163
|
-
path,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
if (err instanceof LEClientError) throw err;
|
|
168
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
169
|
-
throw new LEClientError(
|
|
170
|
-
`LE ${path} timed out after ${LE_TIMEOUT_MS}ms`,
|
|
171
|
-
`Check network connectivity to ${url} and verify Litmus Edge is healthy.`,
|
|
172
|
-
path,
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
throw new LEClientError(
|
|
176
|
-
`LE ${path} request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
|
-
buildLEClientFix(path),
|
|
178
|
-
path,
|
|
179
|
-
);
|
|
180
|
-
} finally {
|
|
181
|
-
clearTimeout(timeout);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { formatThrownError, pMap } from "./le-concurrency.ts";
|
|
3
|
-
|
|
4
|
-
describe("pMap", () => {
|
|
5
|
-
test("returns empty array for empty input", async () => {
|
|
6
|
-
const results = await pMap([], async (x) => x, { concurrency: 5 });
|
|
7
|
-
expect(results).toEqual([]);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
test("returns results in original order", async () => {
|
|
11
|
-
const items = [3, 1, 2];
|
|
12
|
-
const results = await pMap(
|
|
13
|
-
items,
|
|
14
|
-
async (x) => {
|
|
15
|
-
await Bun.sleep(x * 10);
|
|
16
|
-
return x * 10;
|
|
17
|
-
},
|
|
18
|
-
{ concurrency: 3 },
|
|
19
|
-
);
|
|
20
|
-
expect(results).toEqual([30, 10, 20]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("respects concurrency limit", async () => {
|
|
24
|
-
let active = 0;
|
|
25
|
-
let peak = 0;
|
|
26
|
-
const items = Array.from({ length: 10 }, (_, i) => i);
|
|
27
|
-
|
|
28
|
-
await pMap(
|
|
29
|
-
items,
|
|
30
|
-
async () => {
|
|
31
|
-
active++;
|
|
32
|
-
peak = Math.max(peak, active);
|
|
33
|
-
await Bun.sleep(20);
|
|
34
|
-
active--;
|
|
35
|
-
},
|
|
36
|
-
{ concurrency: 3 },
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
expect(peak).toBe(3);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("concurrency=1 runs sequentially", async () => {
|
|
43
|
-
const order: number[] = [];
|
|
44
|
-
const items = [1, 2, 3];
|
|
45
|
-
|
|
46
|
-
await pMap(
|
|
47
|
-
items,
|
|
48
|
-
async (x) => {
|
|
49
|
-
order.push(x);
|
|
50
|
-
await Bun.sleep(5);
|
|
51
|
-
},
|
|
52
|
-
{ concurrency: 1 },
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(order).toEqual([1, 2, 3]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("stopOnError: true stops launching after first error", async () => {
|
|
59
|
-
let launched = 0;
|
|
60
|
-
const items = Array.from({ length: 10 }, (_, i) => i);
|
|
61
|
-
|
|
62
|
-
await expect(
|
|
63
|
-
pMap(
|
|
64
|
-
items,
|
|
65
|
-
async (x) => {
|
|
66
|
-
launched++;
|
|
67
|
-
await Bun.sleep(5);
|
|
68
|
-
if (x === 1) throw new Error("fail at 1");
|
|
69
|
-
},
|
|
70
|
-
{ concurrency: 2, stopOnError: true },
|
|
71
|
-
),
|
|
72
|
-
).rejects.toThrow("fail at 1");
|
|
73
|
-
|
|
74
|
-
// With concurrency 2, at most ~4 launched (2 initial + a couple pulled before error propagates)
|
|
75
|
-
expect(launched).toBeLessThanOrEqual(6);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("stopOnError: true rejects with the first error chronologically", async () => {
|
|
79
|
-
const items = [0, 1, 2];
|
|
80
|
-
|
|
81
|
-
await expect(
|
|
82
|
-
pMap(
|
|
83
|
-
items,
|
|
84
|
-
async (x) => {
|
|
85
|
-
await Bun.sleep(x * 10);
|
|
86
|
-
throw new Error(`fail-${x}`);
|
|
87
|
-
},
|
|
88
|
-
{ concurrency: 3, stopOnError: true },
|
|
89
|
-
),
|
|
90
|
-
).rejects.toThrow("fail-0");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("default (best-effort) continues all items and rejects with AggregateError", async () => {
|
|
94
|
-
const completed: number[] = [];
|
|
95
|
-
const items = [0, 1, 2, 3, 4];
|
|
96
|
-
|
|
97
|
-
await expect(
|
|
98
|
-
pMap(
|
|
99
|
-
items,
|
|
100
|
-
async (x) => {
|
|
101
|
-
await Bun.sleep(5);
|
|
102
|
-
if (x === 1 || x === 3) throw new Error(`fail-${x}`);
|
|
103
|
-
completed.push(x);
|
|
104
|
-
},
|
|
105
|
-
{ concurrency: 2 },
|
|
106
|
-
),
|
|
107
|
-
).rejects.toThrow("2 item(s) failed");
|
|
108
|
-
|
|
109
|
-
// All non-failing items should have completed
|
|
110
|
-
expect(completed.sort()).toEqual([0, 2, 4]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test("default (best-effort) AggregateError contains all individual errors", async () => {
|
|
114
|
-
const items = [0, 1, 2];
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
await pMap(
|
|
118
|
-
items,
|
|
119
|
-
async (x) => {
|
|
120
|
-
await Bun.sleep(5);
|
|
121
|
-
if (x === 0 || x === 2) throw new Error(`fail-${x}`);
|
|
122
|
-
},
|
|
123
|
-
{ concurrency: 3 },
|
|
124
|
-
);
|
|
125
|
-
expect.unreachable("should have thrown");
|
|
126
|
-
} catch (err) {
|
|
127
|
-
expect(err).toBeInstanceOf(AggregateError);
|
|
128
|
-
const agg = err as AggregateError;
|
|
129
|
-
expect(agg.errors).toHaveLength(2);
|
|
130
|
-
const messages = agg.errors.map((e: Error) => e.message).sort();
|
|
131
|
-
expect(messages).toEqual(["fail-0", "fail-2"]);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("passes index to callback", async () => {
|
|
136
|
-
const indices: number[] = [];
|
|
137
|
-
await pMap(["a", "b", "c"], async (_, i) => indices.push(i), { concurrency: 2 });
|
|
138
|
-
expect(indices.sort()).toEqual([0, 1, 2]);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("formatThrownError", () => {
|
|
143
|
-
test("expands AggregateError to underlying messages", () => {
|
|
144
|
-
const agg = new AggregateError(
|
|
145
|
-
[new Error("LE GraphQL: PKI"), new Error("timeout")],
|
|
146
|
-
"2 item(s) failed",
|
|
147
|
-
);
|
|
148
|
-
const out = formatThrownError(agg);
|
|
149
|
-
expect(out).toContain("LE GraphQL: PKI");
|
|
150
|
-
expect(out).toContain("timeout");
|
|
151
|
-
expect(out).not.toBe("2 item(s) failed");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("passes through normal Error", () => {
|
|
155
|
-
expect(formatThrownError(new Error("plain"))).toBe("plain");
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded-concurrency async map for LE API calls.
|
|
3
|
-
*
|
|
4
|
-
* By default (`stopOnError: false`), all items run to completion and
|
|
5
|
-
* failures are collected into an `AggregateError`.
|
|
6
|
-
*
|
|
7
|
-
* With `stopOnError: true`, workers stop launching new tasks on first
|
|
8
|
-
* rejection, await all in-flight, then reject with the earliest error.
|
|
9
|
-
*
|
|
10
|
-
* `formatThrownError` expands `AggregateError` (count-only `.message`) into underlying messages.
|
|
11
|
-
*/
|
|
12
|
-
export function formatThrownError(err: unknown): string {
|
|
13
|
-
if (err instanceof AggregateError) {
|
|
14
|
-
const inner = err.errors.map((e) => (e instanceof Error ? e.message : String(e)));
|
|
15
|
-
const unique = [...new Set(inner.filter((m) => m.length > 0))];
|
|
16
|
-
if (unique.length === 0) return err.message;
|
|
17
|
-
if (unique.length === 1) return unique[0] ?? err.message;
|
|
18
|
-
return `${unique.slice(0, 8).join("\n")}${unique.length > 8 ? `\n… +${unique.length - 8} more` : ""}`;
|
|
19
|
-
}
|
|
20
|
-
if (err instanceof Error) return err.message;
|
|
21
|
-
return String(err);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function pMap<T, R>(
|
|
25
|
-
items: readonly T[],
|
|
26
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
27
|
-
opts: {
|
|
28
|
-
readonly concurrency: number;
|
|
29
|
-
readonly stopOnError?: boolean;
|
|
30
|
-
readonly signal?: AbortSignal;
|
|
31
|
-
},
|
|
32
|
-
): Promise<R[]> {
|
|
33
|
-
if (items.length === 0) return [];
|
|
34
|
-
|
|
35
|
-
const stopOnError = opts.stopOnError ?? false;
|
|
36
|
-
const concurrency = Math.max(1, Math.min(opts.concurrency, items.length));
|
|
37
|
-
const results = new Array<R>(items.length);
|
|
38
|
-
let nextIndex = 0;
|
|
39
|
-
let firstError: unknown;
|
|
40
|
-
let hasError = false;
|
|
41
|
-
const errors: { index: number; error: unknown }[] = [];
|
|
42
|
-
|
|
43
|
-
async function worker(): Promise<void> {
|
|
44
|
-
while (nextIndex < items.length) {
|
|
45
|
-
if (stopOnError && hasError) break;
|
|
46
|
-
if (opts.signal?.aborted) break;
|
|
47
|
-
const idx = nextIndex++;
|
|
48
|
-
// idx could exceed bounds if multiple workers read nextIndex in the same
|
|
49
|
-
// microtask (not possible in single-threaded JS, but defensive).
|
|
50
|
-
if (idx >= items.length) break;
|
|
51
|
-
try {
|
|
52
|
-
results[idx] = await fn(items[idx] as T, idx);
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (!hasError) {
|
|
55
|
-
hasError = true;
|
|
56
|
-
firstError = err;
|
|
57
|
-
}
|
|
58
|
-
if (stopOnError) break;
|
|
59
|
-
errors.push({ index: idx, error: err });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const workers: Promise<void>[] = [];
|
|
65
|
-
for (let i = 0; i < concurrency; i++) {
|
|
66
|
-
workers.push(worker());
|
|
67
|
-
}
|
|
68
|
-
await Promise.all(workers);
|
|
69
|
-
|
|
70
|
-
if (stopOnError && hasError) throw firstError;
|
|
71
|
-
if (errors.length > 0) {
|
|
72
|
-
throw new AggregateError(
|
|
73
|
-
errors.map((e) => e.error),
|
|
74
|
-
`${errors.length} item(s) failed`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
78
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { MiravoError } from "@miravo/shared";
|
|
2
|
-
import { LEClient } from "./le-client.ts";
|
|
3
|
-
import { formatThrownError } from "./le-concurrency.ts";
|
|
4
|
-
import type { LEEnvironment } from "./le-environments.ts";
|
|
5
|
-
import { DEFAULT_PLATFORM_ID } from "./le-environments.ts";
|
|
6
|
-
|
|
7
|
-
type TestLeConnectionResult =
|
|
8
|
-
| { readonly connected: true; readonly hostname: string; readonly sourceHostname: string }
|
|
9
|
-
| { readonly connected: false; readonly error: string };
|
|
10
|
-
|
|
11
|
-
export async function testLeConnection(input: {
|
|
12
|
-
readonly hostname: string;
|
|
13
|
-
readonly apiToken: string;
|
|
14
|
-
readonly sourceHostname: string;
|
|
15
|
-
}): Promise<TestLeConnectionResult> {
|
|
16
|
-
try {
|
|
17
|
-
const client = new LEClient({
|
|
18
|
-
hostname: input.hostname,
|
|
19
|
-
apiToken: input.apiToken,
|
|
20
|
-
});
|
|
21
|
-
await client.checkSysinfoVersion();
|
|
22
|
-
return {
|
|
23
|
-
connected: true,
|
|
24
|
-
hostname: input.hostname,
|
|
25
|
-
sourceHostname: input.sourceHostname,
|
|
26
|
-
};
|
|
27
|
-
} catch (error) {
|
|
28
|
-
return {
|
|
29
|
-
connected: false,
|
|
30
|
-
error: formatThrownError(error),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function assertSupportedEnvironmentPlatform(env: LEEnvironment): void {
|
|
36
|
-
if (env.platformId !== DEFAULT_PLATFORM_ID) {
|
|
37
|
-
throw new MiravoError(
|
|
38
|
-
`Environment "${env.label}" uses unsupported platform "${env.platformId}".`,
|
|
39
|
-
`Use an environment with platform "${DEFAULT_PLATFORM_ID}" for connection tests.`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { buildLeDeviceHubDeviceProperties } from "./le-devicehub-properties.ts";
|
|
3
|
-
|
|
4
|
-
describe("buildLeDeviceHubDeviceProperties", () => {
|
|
5
|
-
test("modbus uses networkAddress, networkPort, and stationId", () => {
|
|
6
|
-
const props = buildLeDeviceHubDeviceProperties("modbus", "10.0.0.50", 502);
|
|
7
|
-
expect(props.map((p) => [p.Key, p.Value])).toEqual([
|
|
8
|
-
["networkAddress", "10.0.0.50"],
|
|
9
|
-
["networkPort", "502"],
|
|
10
|
-
["stationId", "1"],
|
|
11
|
-
]);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
test("opcua uses LE PascalCase keys and numeric security enum values", () => {
|
|
15
|
-
expect(buildLeDeviceHubDeviceProperties("opcua", "miravo.host", 4840)).toEqual([
|
|
16
|
-
{ Key: "ServerURL", Value: "opc.tcp://miravo.host:4840" },
|
|
17
|
-
{ Key: "SecurityPolicy", Value: "0" },
|
|
18
|
-
{ Key: "SecurityMode", Value: "0" },
|
|
19
|
-
]);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("mqtt uses LE PascalCase keys: BrokerAddress and Port", () => {
|
|
23
|
-
expect(buildLeDeviceHubDeviceProperties("mqtt", "miravo.host", 1883)).toEqual([
|
|
24
|
-
{ Key: "BrokerAddress", Value: "miravo.host" },
|
|
25
|
-
{ Key: "Port", Value: "1883" },
|
|
26
|
-
]);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("opcua ServerURL encodes non-default port", () => {
|
|
30
|
-
const props = buildLeDeviceHubDeviceProperties("opcua", "plc.local", 48400);
|
|
31
|
-
const urlProp = props.find((p) => p.Key === "ServerURL");
|
|
32
|
-
expect(urlProp?.Value).toBe("opc.tcp://plc.local:48400");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("no key uses legacy names rejected or ignored by LE", () => {
|
|
36
|
-
const banned = new Set([
|
|
37
|
-
"serverUrl",
|
|
38
|
-
"securityPolicy",
|
|
39
|
-
"securityMode",
|
|
40
|
-
"brokerAddress",
|
|
41
|
-
"brokerPort",
|
|
42
|
-
"endpoint",
|
|
43
|
-
"host",
|
|
44
|
-
"url",
|
|
45
|
-
]);
|
|
46
|
-
for (const protocol of ["opcua", "modbus", "mqtt"] as const) {
|
|
47
|
-
const props = buildLeDeviceHubDeviceProperties(protocol, "h", 1);
|
|
48
|
-
for (const { Key } of props) {
|
|
49
|
-
expect(banned.has(Key)).toBe(false);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** Key/value pairs for Litmus Edge device `settings` (apply + offline template export). */
|
|
2
|
-
|
|
3
|
-
import type { ProtocolName } from "@miravo/shared";
|
|
4
|
-
|
|
5
|
-
export function buildLeDeviceHubDeviceProperties(
|
|
6
|
-
protocol: ProtocolName,
|
|
7
|
-
sourceHostname: string,
|
|
8
|
-
endpointPort: number,
|
|
9
|
-
): readonly { Key: string; Value: string }[] {
|
|
10
|
-
switch (protocol) {
|
|
11
|
-
case "opcua":
|
|
12
|
-
return [
|
|
13
|
-
{ Key: "ServerURL", Value: `opc.tcp://${sourceHostname}:${endpointPort}` },
|
|
14
|
-
{ Key: "SecurityPolicy", Value: "0" },
|
|
15
|
-
{ Key: "SecurityMode", Value: "0" },
|
|
16
|
-
];
|
|
17
|
-
case "modbus":
|
|
18
|
-
return [
|
|
19
|
-
{ Key: "networkAddress", Value: sourceHostname },
|
|
20
|
-
{ Key: "networkPort", Value: String(endpointPort) },
|
|
21
|
-
{ Key: "stationId", Value: "1" },
|
|
22
|
-
];
|
|
23
|
-
case "mqtt":
|
|
24
|
-
return [
|
|
25
|
-
{ Key: "BrokerAddress", Value: sourceHostname },
|
|
26
|
-
{ Key: "Port", Value: String(endpointPort) },
|
|
27
|
-
];
|
|
28
|
-
default: {
|
|
29
|
-
const _never: never = protocol;
|
|
30
|
-
throw new Error(`Unknown protocol: ${_never}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|