proschi 0.11.0 → 0.13.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/dist/cli.cjs +1526 -279
- package/dist/render.cjs +346 -203
- package/dist/server.cjs +1101 -126
- package/grammar/proschi.tmLanguage.json +90 -0
- package/package.json +7 -6
package/dist/server.cjs
CHANGED
|
@@ -1303,8 +1303,8 @@ var require_semaphore = __commonJS({
|
|
|
1303
1303
|
this._waiting = [];
|
|
1304
1304
|
}
|
|
1305
1305
|
lock(thunk) {
|
|
1306
|
-
return new Promise((
|
|
1307
|
-
this._waiting.push({ thunk, resolve:
|
|
1306
|
+
return new Promise((resolve3, reject) => {
|
|
1307
|
+
this._waiting.push({ thunk, resolve: resolve3, reject });
|
|
1308
1308
|
this.runNext();
|
|
1309
1309
|
});
|
|
1310
1310
|
}
|
|
@@ -2930,9 +2930,9 @@ ${JSON.stringify(message, null, 4)}`);
|
|
|
2930
2930
|
if (typeof cancellationStrategy.sender.enableCancellation === "function") {
|
|
2931
2931
|
cancellationStrategy.sender.enableCancellation(requestMessage);
|
|
2932
2932
|
}
|
|
2933
|
-
return new Promise(async (
|
|
2933
|
+
return new Promise(async (resolve3, reject) => {
|
|
2934
2934
|
const resolveWithCleanup = (r) => {
|
|
2935
|
-
|
|
2935
|
+
resolve3(r);
|
|
2936
2936
|
cancellationStrategy.sender.cleanup(id);
|
|
2937
2937
|
disposable?.dispose();
|
|
2938
2938
|
};
|
|
@@ -8574,8 +8574,8 @@ var require_server = __commonJS({
|
|
|
8574
8574
|
if (value instanceof Promise) {
|
|
8575
8575
|
return value;
|
|
8576
8576
|
} else if (Is2.thenable(value)) {
|
|
8577
|
-
return new Promise((
|
|
8578
|
-
value.then((resolved) =>
|
|
8577
|
+
return new Promise((resolve3, reject) => {
|
|
8578
|
+
value.then((resolved) => resolve3(resolved), (error) => reject(error));
|
|
8579
8579
|
});
|
|
8580
8580
|
} else {
|
|
8581
8581
|
return Promise.resolve(value);
|
|
@@ -8859,7 +8859,7 @@ var require_files = __commonJS({
|
|
|
8859
8859
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
8860
8860
|
exports2.FileSystem = void 0;
|
|
8861
8861
|
exports2.uriToFilePath = uriToFilePath;
|
|
8862
|
-
exports2.resolve =
|
|
8862
|
+
exports2.resolve = resolve3;
|
|
8863
8863
|
exports2.resolveGlobalNodePath = resolveGlobalNodePath;
|
|
8864
8864
|
exports2.resolveGlobalYarnPath = resolveGlobalYarnPath;
|
|
8865
8865
|
exports2.resolveModulePath = resolveModulePath;
|
|
@@ -8888,7 +8888,7 @@ var require_files = __commonJS({
|
|
|
8888
8888
|
function isWindows() {
|
|
8889
8889
|
return process.platform === "win32";
|
|
8890
8890
|
}
|
|
8891
|
-
function
|
|
8891
|
+
function resolve3(moduleName, nodePath, cwd, tracer) {
|
|
8892
8892
|
const nodePathKey = "NODE_PATH";
|
|
8893
8893
|
const app = [
|
|
8894
8894
|
"var p = process;",
|
|
@@ -8907,7 +8907,7 @@ var require_files = __commonJS({
|
|
|
8907
8907
|
"}",
|
|
8908
8908
|
"});"
|
|
8909
8909
|
].join("");
|
|
8910
|
-
return new Promise((
|
|
8910
|
+
return new Promise((resolve4, reject) => {
|
|
8911
8911
|
const env = process.env;
|
|
8912
8912
|
const newEnv = /* @__PURE__ */ Object.create(null);
|
|
8913
8913
|
Object.keys(env).forEach((key) => newEnv[key] = env[key]);
|
|
@@ -8939,7 +8939,7 @@ var require_files = __commonJS({
|
|
|
8939
8939
|
if (message2.c === "r") {
|
|
8940
8940
|
cp.send({ c: "e" });
|
|
8941
8941
|
if (message2.s) {
|
|
8942
|
-
|
|
8942
|
+
resolve4(message2.r);
|
|
8943
8943
|
} else {
|
|
8944
8944
|
reject(new Error(`Failed to resolve module: ${moduleName}`));
|
|
8945
8945
|
}
|
|
@@ -9067,17 +9067,17 @@ var require_files = __commonJS({
|
|
|
9067
9067
|
if (!path.isAbsolute(nodePath)) {
|
|
9068
9068
|
nodePath = path.join(workspaceRoot, nodePath);
|
|
9069
9069
|
}
|
|
9070
|
-
return
|
|
9070
|
+
return resolve3(moduleName, nodePath, nodePath, tracer).then((value) => {
|
|
9071
9071
|
if (FileSystem.isParent(nodePath, value)) {
|
|
9072
9072
|
return value;
|
|
9073
9073
|
} else {
|
|
9074
9074
|
return Promise.reject(new Error(`Failed to load ${moduleName} from node path location.`));
|
|
9075
9075
|
}
|
|
9076
9076
|
}).then(void 0, (_error) => {
|
|
9077
|
-
return
|
|
9077
|
+
return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
|
|
9078
9078
|
});
|
|
9079
9079
|
} else {
|
|
9080
|
-
return
|
|
9080
|
+
return resolve3(moduleName, resolveGlobalNodePath(tracer), workspaceRoot, tracer);
|
|
9081
9081
|
}
|
|
9082
9082
|
}
|
|
9083
9083
|
}
|
|
@@ -9159,10 +9159,10 @@ var require_ril = __commonJS({
|
|
|
9159
9159
|
return api_1.Disposable.create(() => this.stream.off("end", listener));
|
|
9160
9160
|
}
|
|
9161
9161
|
write(data, encoding) {
|
|
9162
|
-
return new Promise((
|
|
9162
|
+
return new Promise((resolve3, reject) => {
|
|
9163
9163
|
const callback = (error) => {
|
|
9164
9164
|
if (error === void 0 || error === null) {
|
|
9165
|
-
|
|
9165
|
+
resolve3();
|
|
9166
9166
|
} else {
|
|
9167
9167
|
reject(error);
|
|
9168
9168
|
}
|
|
@@ -9454,10 +9454,10 @@ var require_main = __commonJS({
|
|
|
9454
9454
|
}
|
|
9455
9455
|
function createClientPipeTransport(pipeName, encoding = "utf-8") {
|
|
9456
9456
|
let connectResolve;
|
|
9457
|
-
const connected = new Promise((
|
|
9458
|
-
connectResolve =
|
|
9457
|
+
const connected = new Promise((resolve3, _reject) => {
|
|
9458
|
+
connectResolve = resolve3;
|
|
9459
9459
|
});
|
|
9460
|
-
return new Promise((
|
|
9460
|
+
return new Promise((resolve3, reject) => {
|
|
9461
9461
|
const server = (0, net_1.createServer)((socket) => {
|
|
9462
9462
|
server.close();
|
|
9463
9463
|
connectResolve([
|
|
@@ -9468,7 +9468,7 @@ var require_main = __commonJS({
|
|
|
9468
9468
|
server.on("error", reject);
|
|
9469
9469
|
server.listen(pipeName, () => {
|
|
9470
9470
|
server.removeListener("error", reject);
|
|
9471
|
-
|
|
9471
|
+
resolve3({
|
|
9472
9472
|
onConnected: () => {
|
|
9473
9473
|
return connected;
|
|
9474
9474
|
}
|
|
@@ -9485,10 +9485,10 @@ var require_main = __commonJS({
|
|
|
9485
9485
|
}
|
|
9486
9486
|
function createClientSocketTransport(port, encoding = "utf-8") {
|
|
9487
9487
|
let connectResolve;
|
|
9488
|
-
const connected = new Promise((
|
|
9489
|
-
connectResolve =
|
|
9488
|
+
const connected = new Promise((resolve3, _reject) => {
|
|
9489
|
+
connectResolve = resolve3;
|
|
9490
9490
|
});
|
|
9491
|
-
return new Promise((
|
|
9491
|
+
return new Promise((resolve3, reject) => {
|
|
9492
9492
|
const server = (0, net_1.createServer)((socket) => {
|
|
9493
9493
|
server.close();
|
|
9494
9494
|
connectResolve([
|
|
@@ -9505,7 +9505,7 @@ var require_main = __commonJS({
|
|
|
9505
9505
|
return;
|
|
9506
9506
|
}
|
|
9507
9507
|
const boundPort = address.port;
|
|
9508
|
-
|
|
9508
|
+
resolve3({
|
|
9509
9509
|
port: () => boundPort,
|
|
9510
9510
|
onConnected: () => {
|
|
9511
9511
|
return connected;
|
|
@@ -12856,7 +12856,7 @@ var require_compile = __commonJS({
|
|
|
12856
12856
|
const schOrFunc = root.refs[ref];
|
|
12857
12857
|
if (schOrFunc)
|
|
12858
12858
|
return schOrFunc;
|
|
12859
|
-
let _sch =
|
|
12859
|
+
let _sch = resolve3.call(this, root, ref);
|
|
12860
12860
|
if (_sch === void 0) {
|
|
12861
12861
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
12862
12862
|
const { schemaId } = this.opts;
|
|
@@ -12883,7 +12883,7 @@ var require_compile = __commonJS({
|
|
|
12883
12883
|
function sameSchemaEnv(s1, s2) {
|
|
12884
12884
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
12885
12885
|
}
|
|
12886
|
-
function
|
|
12886
|
+
function resolve3(root, ref) {
|
|
12887
12887
|
let sch;
|
|
12888
12888
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
12889
12889
|
ref = sch;
|
|
@@ -13713,7 +13713,7 @@ var require_fast_uri = __commonJS({
|
|
|
13713
13713
|
}
|
|
13714
13714
|
return uri;
|
|
13715
13715
|
}
|
|
13716
|
-
function
|
|
13716
|
+
function resolve3(baseURI, relativeURI, options) {
|
|
13717
13717
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
13718
13718
|
const {
|
|
13719
13719
|
parsed: baseParsed,
|
|
@@ -14082,7 +14082,7 @@ var require_fast_uri = __commonJS({
|
|
|
14082
14082
|
var fastUri = {
|
|
14083
14083
|
SCHEMES,
|
|
14084
14084
|
normalize,
|
|
14085
|
-
resolve:
|
|
14085
|
+
resolve: resolve3,
|
|
14086
14086
|
resolveComponent,
|
|
14087
14087
|
equal,
|
|
14088
14088
|
serialize,
|
|
@@ -21292,10 +21292,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
21292
21292
|
let offset = bm.offset;
|
|
21293
21293
|
let commentEnd = null;
|
|
21294
21294
|
for (const collItem of bm.items) {
|
|
21295
|
-
const { start, key, sep, value } = collItem;
|
|
21295
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
21296
21296
|
const keyProps = resolveProps.resolveProps(start, {
|
|
21297
21297
|
indicator: "explicit-key-ind",
|
|
21298
|
-
next: key ??
|
|
21298
|
+
next: key ?? sep2?.[0],
|
|
21299
21299
|
offset,
|
|
21300
21300
|
onError,
|
|
21301
21301
|
parentIndent: bm.indent,
|
|
@@ -21309,7 +21309,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21309
21309
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
21310
21310
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
21311
21311
|
}
|
|
21312
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
21312
|
+
if (!keyProps.anchor && !keyProps.tag && !sep2) {
|
|
21313
21313
|
commentEnd = keyProps.end;
|
|
21314
21314
|
if (keyProps.comment) {
|
|
21315
21315
|
if (map.comment)
|
|
@@ -21333,7 +21333,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21333
21333
|
ctx.atKey = false;
|
|
21334
21334
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
21335
21335
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
21336
|
-
const valueProps = resolveProps.resolveProps(
|
|
21336
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
21337
21337
|
indicator: "map-value-ind",
|
|
21338
21338
|
next: value,
|
|
21339
21339
|
offset: keyNode.range[2],
|
|
@@ -21349,7 +21349,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
21349
21349
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
21350
21350
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
21351
21351
|
}
|
|
21352
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
21352
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep2, null, valueProps, onError);
|
|
21353
21353
|
if (ctx.schema.compat)
|
|
21354
21354
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
21355
21355
|
offset = valueNode.range[2];
|
|
@@ -21440,7 +21440,7 @@ var require_resolve_end = __commonJS({
|
|
|
21440
21440
|
let comment = "";
|
|
21441
21441
|
if (end) {
|
|
21442
21442
|
let hasSpace = false;
|
|
21443
|
-
let
|
|
21443
|
+
let sep2 = "";
|
|
21444
21444
|
for (const token of end) {
|
|
21445
21445
|
const { source: source2, type } = token;
|
|
21446
21446
|
switch (type) {
|
|
@@ -21454,13 +21454,13 @@ var require_resolve_end = __commonJS({
|
|
|
21454
21454
|
if (!comment)
|
|
21455
21455
|
comment = cb;
|
|
21456
21456
|
else
|
|
21457
|
-
comment +=
|
|
21458
|
-
|
|
21457
|
+
comment += sep2 + cb;
|
|
21458
|
+
sep2 = "";
|
|
21459
21459
|
break;
|
|
21460
21460
|
}
|
|
21461
21461
|
case "newline":
|
|
21462
21462
|
if (comment)
|
|
21463
|
-
|
|
21463
|
+
sep2 += source2;
|
|
21464
21464
|
hasSpace = true;
|
|
21465
21465
|
break;
|
|
21466
21466
|
default:
|
|
@@ -21503,18 +21503,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21503
21503
|
let offset = fc.offset + fc.start.source.length;
|
|
21504
21504
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
21505
21505
|
const collItem = fc.items[i];
|
|
21506
|
-
const { start, key, sep, value } = collItem;
|
|
21506
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
21507
21507
|
const props = resolveProps.resolveProps(start, {
|
|
21508
21508
|
flow: fcName,
|
|
21509
21509
|
indicator: "explicit-key-ind",
|
|
21510
|
-
next: key ??
|
|
21510
|
+
next: key ?? sep2?.[0],
|
|
21511
21511
|
offset,
|
|
21512
21512
|
onError,
|
|
21513
21513
|
parentIndent: fc.indent,
|
|
21514
21514
|
startOnNewline: false
|
|
21515
21515
|
});
|
|
21516
21516
|
if (!props.found) {
|
|
21517
|
-
if (!props.anchor && !props.tag && !
|
|
21517
|
+
if (!props.anchor && !props.tag && !sep2 && !value) {
|
|
21518
21518
|
if (i === 0 && props.comma)
|
|
21519
21519
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
21520
21520
|
else if (i < fc.items.length - 1)
|
|
@@ -21568,8 +21568,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21568
21568
|
}
|
|
21569
21569
|
}
|
|
21570
21570
|
}
|
|
21571
|
-
if (!isMap && !
|
|
21572
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
21571
|
+
if (!isMap && !sep2 && !props.found) {
|
|
21572
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
|
|
21573
21573
|
coll.items.push(valueNode);
|
|
21574
21574
|
offset = valueNode.range[2];
|
|
21575
21575
|
if (isBlock(value))
|
|
@@ -21581,7 +21581,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21581
21581
|
if (isBlock(key))
|
|
21582
21582
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
21583
21583
|
ctx.atKey = false;
|
|
21584
|
-
const valueProps = resolveProps.resolveProps(
|
|
21584
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
21585
21585
|
flow: fcName,
|
|
21586
21586
|
indicator: "map-value-ind",
|
|
21587
21587
|
next: value,
|
|
@@ -21592,8 +21592,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21592
21592
|
});
|
|
21593
21593
|
if (valueProps.found) {
|
|
21594
21594
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
21595
|
-
if (
|
|
21596
|
-
for (const st of
|
|
21595
|
+
if (sep2)
|
|
21596
|
+
for (const st of sep2) {
|
|
21597
21597
|
if (st === valueProps.found)
|
|
21598
21598
|
break;
|
|
21599
21599
|
if (st.type === "newline") {
|
|
@@ -21610,7 +21610,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
21610
21610
|
else
|
|
21611
21611
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
21612
21612
|
}
|
|
21613
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
21613
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep2, null, valueProps, onError) : null;
|
|
21614
21614
|
if (valueNode) {
|
|
21615
21615
|
if (isBlock(value))
|
|
21616
21616
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -21790,7 +21790,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
21790
21790
|
chompStart = i + 1;
|
|
21791
21791
|
}
|
|
21792
21792
|
let value = "";
|
|
21793
|
-
let
|
|
21793
|
+
let sep2 = "";
|
|
21794
21794
|
let prevMoreIndented = false;
|
|
21795
21795
|
for (let i = 0; i < contentStart; ++i)
|
|
21796
21796
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -21807,24 +21807,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
21807
21807
|
indent2 = "";
|
|
21808
21808
|
}
|
|
21809
21809
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
21810
|
-
value +=
|
|
21811
|
-
|
|
21810
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
21811
|
+
sep2 = "\n";
|
|
21812
21812
|
} else if (indent2.length > trimIndent || content[0] === " ") {
|
|
21813
|
-
if (
|
|
21814
|
-
|
|
21815
|
-
else if (!prevMoreIndented &&
|
|
21816
|
-
|
|
21817
|
-
value +=
|
|
21818
|
-
|
|
21813
|
+
if (sep2 === " ")
|
|
21814
|
+
sep2 = "\n";
|
|
21815
|
+
else if (!prevMoreIndented && sep2 === "\n")
|
|
21816
|
+
sep2 = "\n\n";
|
|
21817
|
+
value += sep2 + indent2.slice(trimIndent) + content;
|
|
21818
|
+
sep2 = "\n";
|
|
21819
21819
|
prevMoreIndented = true;
|
|
21820
21820
|
} else if (content === "") {
|
|
21821
|
-
if (
|
|
21821
|
+
if (sep2 === "\n")
|
|
21822
21822
|
value += "\n";
|
|
21823
21823
|
else
|
|
21824
|
-
|
|
21824
|
+
sep2 = "\n";
|
|
21825
21825
|
} else {
|
|
21826
|
-
value +=
|
|
21827
|
-
|
|
21826
|
+
value += sep2 + content;
|
|
21827
|
+
sep2 = " ";
|
|
21828
21828
|
prevMoreIndented = false;
|
|
21829
21829
|
}
|
|
21830
21830
|
}
|
|
@@ -22007,25 +22007,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
22007
22007
|
trimBoth = /^[ \t]+|[ \t]+$/g;
|
|
22008
22008
|
}
|
|
22009
22009
|
let res = match[1].replace(trimEnd, "");
|
|
22010
|
-
let
|
|
22010
|
+
let sep2 = " ";
|
|
22011
22011
|
let pos = line.lastIndex;
|
|
22012
22012
|
while (match = line.exec(source2)) {
|
|
22013
22013
|
const lm = match[1].replace(trimBoth, "");
|
|
22014
22014
|
if (lm === "") {
|
|
22015
|
-
if (
|
|
22016
|
-
res +=
|
|
22015
|
+
if (sep2 === "\n")
|
|
22016
|
+
res += sep2;
|
|
22017
22017
|
else
|
|
22018
|
-
|
|
22018
|
+
sep2 = "\n";
|
|
22019
22019
|
} else {
|
|
22020
|
-
res +=
|
|
22021
|
-
|
|
22020
|
+
res += sep2 + lm;
|
|
22021
|
+
sep2 = " ";
|
|
22022
22022
|
}
|
|
22023
22023
|
pos = line.lastIndex;
|
|
22024
22024
|
}
|
|
22025
22025
|
const last = /[ \t]*(.*)/sy;
|
|
22026
22026
|
last.lastIndex = pos;
|
|
22027
22027
|
match = last.exec(source2);
|
|
22028
|
-
return res +
|
|
22028
|
+
return res + sep2 + (match?.[1] ?? "");
|
|
22029
22029
|
}
|
|
22030
22030
|
function doubleQuotedValue(source2, onError) {
|
|
22031
22031
|
let res = "";
|
|
@@ -22835,14 +22835,14 @@ var require_cst_stringify = __commonJS({
|
|
|
22835
22835
|
}
|
|
22836
22836
|
}
|
|
22837
22837
|
}
|
|
22838
|
-
function stringifyItem({ start, key, sep, value }) {
|
|
22838
|
+
function stringifyItem({ start, key, sep: sep2, value }) {
|
|
22839
22839
|
let res = "";
|
|
22840
22840
|
for (const st of start)
|
|
22841
22841
|
res += st.source;
|
|
22842
22842
|
if (key)
|
|
22843
22843
|
res += stringifyToken(key);
|
|
22844
|
-
if (
|
|
22845
|
-
for (const st of
|
|
22844
|
+
if (sep2)
|
|
22845
|
+
for (const st of sep2)
|
|
22846
22846
|
res += st.source;
|
|
22847
22847
|
if (value)
|
|
22848
22848
|
res += stringifyToken(value);
|
|
@@ -24009,18 +24009,18 @@ var require_parser = __commonJS({
|
|
|
24009
24009
|
if (this.type === "map-value-ind") {
|
|
24010
24010
|
const prev = getPrevProps(this.peek(2));
|
|
24011
24011
|
const start = getFirstKeyStartProps(prev);
|
|
24012
|
-
let
|
|
24012
|
+
let sep2;
|
|
24013
24013
|
if (scalar.end) {
|
|
24014
|
-
|
|
24015
|
-
|
|
24014
|
+
sep2 = scalar.end;
|
|
24015
|
+
sep2.push(this.sourceToken);
|
|
24016
24016
|
delete scalar.end;
|
|
24017
24017
|
} else
|
|
24018
|
-
|
|
24018
|
+
sep2 = [this.sourceToken];
|
|
24019
24019
|
const map = {
|
|
24020
24020
|
type: "block-map",
|
|
24021
24021
|
offset: scalar.offset,
|
|
24022
24022
|
indent: scalar.indent,
|
|
24023
|
-
items: [{ start, key: scalar, sep }]
|
|
24023
|
+
items: [{ start, key: scalar, sep: sep2 }]
|
|
24024
24024
|
};
|
|
24025
24025
|
this.onKeyLine = true;
|
|
24026
24026
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -24173,15 +24173,15 @@ var require_parser = __commonJS({
|
|
|
24173
24173
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
24174
24174
|
const start2 = getFirstKeyStartProps(it.start);
|
|
24175
24175
|
const key = it.key;
|
|
24176
|
-
const
|
|
24177
|
-
|
|
24176
|
+
const sep2 = it.sep;
|
|
24177
|
+
sep2.push(this.sourceToken);
|
|
24178
24178
|
delete it.key;
|
|
24179
24179
|
delete it.sep;
|
|
24180
24180
|
this.stack.push({
|
|
24181
24181
|
type: "block-map",
|
|
24182
24182
|
offset: this.offset,
|
|
24183
24183
|
indent: this.indent,
|
|
24184
|
-
items: [{ start: start2, key, sep }]
|
|
24184
|
+
items: [{ start: start2, key, sep: sep2 }]
|
|
24185
24185
|
});
|
|
24186
24186
|
} else if (start.length > 0) {
|
|
24187
24187
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -24375,13 +24375,13 @@ var require_parser = __commonJS({
|
|
|
24375
24375
|
const prev = getPrevProps(parent);
|
|
24376
24376
|
const start = getFirstKeyStartProps(prev);
|
|
24377
24377
|
fixFlowSeqItems(fc);
|
|
24378
|
-
const
|
|
24379
|
-
|
|
24378
|
+
const sep2 = fc.end.splice(1, fc.end.length);
|
|
24379
|
+
sep2.push(this.sourceToken);
|
|
24380
24380
|
const map = {
|
|
24381
24381
|
type: "block-map",
|
|
24382
24382
|
offset: fc.offset,
|
|
24383
24383
|
indent: fc.indent,
|
|
24384
|
-
items: [{ start, key: fc, sep }]
|
|
24384
|
+
items: [{ start, key: fc, sep: sep2 }]
|
|
24385
24385
|
};
|
|
24386
24386
|
this.onKeyLine = true;
|
|
24387
24387
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -24912,7 +24912,7 @@ function getWellformedEdit(textEdit) {
|
|
|
24912
24912
|
}
|
|
24913
24913
|
|
|
24914
24914
|
// src/server.ts
|
|
24915
|
-
var
|
|
24915
|
+
var import_node_fs5 = require("node:fs");
|
|
24916
24916
|
|
|
24917
24917
|
// src/openapi/config.ts
|
|
24918
24918
|
var import_node_fs2 = require("node:fs");
|
|
@@ -25933,6 +25933,8 @@ function editDistance2(a, b) {
|
|
|
25933
25933
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
25934
25934
|
var DEFAULT_TECH = "Rectangle";
|
|
25935
25935
|
var DEFAULT_GROUP_TECH = "Logical Group";
|
|
25936
|
+
var SYSTEM_TECH = "Service";
|
|
25937
|
+
var STUB_HELP = "a rate, p99, latency or availability, e.g. capacity { 2k rps p99 150ms availability 99.95% }";
|
|
25936
25938
|
var MAX_SCENARIOS = 32;
|
|
25937
25939
|
var MAX_ALT_DEPTH = 16;
|
|
25938
25940
|
var DIRECTIONS = ["down", "right", "up", "left"];
|
|
@@ -25977,7 +25979,7 @@ var DID_YOU_MEAN = /^Unknown tech stack '(.*)'\. Did you mean '([^']+)'\?/;
|
|
|
25977
25979
|
function parse(source2, options = {}) {
|
|
25978
25980
|
return new Parser(source2, options).run();
|
|
25979
25981
|
}
|
|
25980
|
-
var Parser = class {
|
|
25982
|
+
var Parser = class _Parser {
|
|
25981
25983
|
// Per-file state, swapped while an imported file is read.
|
|
25982
25984
|
lines;
|
|
25983
25985
|
/** Path of the imported file being read; undefined for the root document. */
|
|
@@ -25998,7 +26000,12 @@ var Parser = class {
|
|
|
25998
26000
|
/** Files being read, outermost first, to find import cycles. */
|
|
25999
26001
|
chain;
|
|
26000
26002
|
/** Diagnostics are sorted by file in the order the files were first read, the root first. */
|
|
26001
|
-
fileOrder
|
|
26003
|
+
fileOrder;
|
|
26004
|
+
/** The file read as the root: undefined for a document, the path for a system's team file (its own scope). */
|
|
26005
|
+
rootFile;
|
|
26006
|
+
teamFiles;
|
|
26007
|
+
systems = [];
|
|
26008
|
+
systemById = /* @__PURE__ */ new Map();
|
|
26002
26009
|
/** `a|b` for every connection, both directions; filled before use cases are built. */
|
|
26003
26010
|
connected = /* @__PURE__ */ new Set();
|
|
26004
26011
|
title;
|
|
@@ -26020,14 +26027,18 @@ var Parser = class {
|
|
|
26020
26027
|
deferred = [];
|
|
26021
26028
|
built = [];
|
|
26022
26029
|
options;
|
|
26023
|
-
constructor(source2, options) {
|
|
26030
|
+
constructor(source2, options, scope2) {
|
|
26024
26031
|
this.options = options;
|
|
26025
26032
|
this.lines = source2.split("\n");
|
|
26026
|
-
this.chain = [options.path];
|
|
26033
|
+
this.chain = scope2 ? scope2.chain : [options.path];
|
|
26034
|
+
this.teamFiles = scope2?.teamFiles ?? /* @__PURE__ */ new Map();
|
|
26035
|
+
if (scope2) this.rootFile = this.file = options.path;
|
|
26036
|
+
this.fileOrder = /* @__PURE__ */ new Map([[this.rootFile, 0]]);
|
|
26027
26037
|
if (options.path !== void 0) this.included.add(options.path);
|
|
26028
26038
|
}
|
|
26029
26039
|
run() {
|
|
26030
26040
|
this.parseFile();
|
|
26041
|
+
this.resolvePorts();
|
|
26031
26042
|
this.createImplicitNodes();
|
|
26032
26043
|
this.resolveTags();
|
|
26033
26044
|
for (const e of this.edges) this.connected.add(`${e.source}|${e.target}`).add(`${e.target}|${e.source}`);
|
|
@@ -26050,7 +26061,8 @@ var Parser = class {
|
|
|
26050
26061
|
...this.decisions.length ? { decisions: this.decisions } : {},
|
|
26051
26062
|
...this.tests.length ? { tests: this.tests } : {},
|
|
26052
26063
|
...this.legend.length ? { legend: this.legend } : {},
|
|
26053
|
-
...Object.keys(this.layout).length ? { layout: this.layout } : {}
|
|
26064
|
+
...Object.keys(this.layout).length ? { layout: this.layout } : {},
|
|
26065
|
+
...this.systems.length ? { systems: this.systems } : {}
|
|
26054
26066
|
};
|
|
26055
26067
|
const order = (d) => this.fileOrder.get(d.file) ?? 0;
|
|
26056
26068
|
this.diagnostics.sort((a, b) => order(a) - order(b) || a.line - b.line || a.col - b.col);
|
|
@@ -26067,14 +26079,16 @@ var Parser = class {
|
|
|
26067
26079
|
i = this.parseLine(i);
|
|
26068
26080
|
}
|
|
26069
26081
|
for (const frame of this.stack) {
|
|
26070
|
-
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : sectionName(frame);
|
|
26082
|
+
const what = frame.kind === "group" ? `group '${frame.id}'` : frame.kind === "usecase" ? `usecase '${frame.useCase.name}'` : frame.kind === "alt" ? `alt '${frame.name}'` : frame.kind === "par" ? "par block" : frame.kind === "system" ? `system '${frame.system.id}'` : frame.kind === "stub" ? `the capacity of system '${frame.system.id}'` : sectionName(frame);
|
|
26071
26083
|
this.error(`Missing } to close ${what}`, frame.loc);
|
|
26084
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
26072
26085
|
}
|
|
26073
26086
|
}
|
|
26074
26087
|
/** Parses the line at `index` and returns the index of the last line it consumed. */
|
|
26075
26088
|
parseLine(index) {
|
|
26076
26089
|
const line = index + 1;
|
|
26077
|
-
const { tokens, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
26090
|
+
const { tokens: lexed, diagnostics } = tokenizeLine(this.lines[index], line);
|
|
26091
|
+
const tokens = qualify(lexed);
|
|
26078
26092
|
this.diagnostics.push(...diagnostics.map((d) => ({ ...d, ...this.fileField() })));
|
|
26079
26093
|
if (tokens.length === 0 || diagnostics.some((d) => d.severity === "error")) return index;
|
|
26080
26094
|
const [first, second, third] = tokens;
|
|
@@ -26095,13 +26109,17 @@ var Parser = class {
|
|
|
26095
26109
|
if (first.kind === "rbrace") {
|
|
26096
26110
|
this.stack.pop();
|
|
26097
26111
|
if (!top.discard) this.closeSection(top);
|
|
26098
|
-
if (!top.discard && this.file ===
|
|
26112
|
+
if (!top.discard && this.file === this.rootFile) this.blocks.push({ kind: top.section, start: top.loc.line, end: line });
|
|
26099
26113
|
if (second) this.error("Unexpected input after }", loc(second));
|
|
26100
26114
|
} else if (!top.discard) {
|
|
26101
26115
|
this.parseSectionLine(top, tokens, line);
|
|
26102
26116
|
}
|
|
26103
26117
|
return index;
|
|
26104
26118
|
}
|
|
26119
|
+
if (top?.kind === "system" || top?.kind === "stub") {
|
|
26120
|
+
this.parseSystemLine(top, tokens, line);
|
|
26121
|
+
return index;
|
|
26122
|
+
}
|
|
26105
26123
|
if (first.kind === "rbrace") {
|
|
26106
26124
|
if (this.stack.length === 0) this.error("Unmatched }", loc(first));
|
|
26107
26125
|
else this.closeFrame();
|
|
@@ -26123,6 +26141,11 @@ var Parser = class {
|
|
|
26123
26141
|
case "group":
|
|
26124
26142
|
this.parseGroup(tokens, line);
|
|
26125
26143
|
break;
|
|
26144
|
+
// A keyword only as `system id … {`, so a node may still be called system.
|
|
26145
|
+
case "system":
|
|
26146
|
+
if (second?.kind === "ident" && tokens[tokens.length - 1].kind === "lbrace") this.parseSystem(tokens, line);
|
|
26147
|
+
else this.parseNode(tokens, line);
|
|
26148
|
+
break;
|
|
26126
26149
|
case "usecase":
|
|
26127
26150
|
this.parseUseCase(tokens, line);
|
|
26128
26151
|
break;
|
|
@@ -26177,7 +26200,7 @@ var Parser = class {
|
|
|
26177
26200
|
return;
|
|
26178
26201
|
}
|
|
26179
26202
|
const summary = tokens[2]?.kind === "string" ? tokens[2] : void 0;
|
|
26180
|
-
if (this.file ===
|
|
26203
|
+
if (this.file === this.rootFile) {
|
|
26181
26204
|
this.title = value.value;
|
|
26182
26205
|
if (summary) this.summary = summary.value;
|
|
26183
26206
|
}
|
|
@@ -26209,7 +26232,7 @@ var Parser = class {
|
|
|
26209
26232
|
this.error("direction is only allowed at the top level or inside a group", this.locOf(keyword, line));
|
|
26210
26233
|
return;
|
|
26211
26234
|
}
|
|
26212
|
-
if (this.file !==
|
|
26235
|
+
if (this.file !== this.rootFile) return;
|
|
26213
26236
|
if (this.layoutLocs.direction) {
|
|
26214
26237
|
this.warning(`direction is already set ${this.where(this.layoutLocs.direction, loc)}; this one is ignored`, this.locOf(keyword, line));
|
|
26215
26238
|
return;
|
|
@@ -26229,7 +26252,7 @@ var Parser = class {
|
|
|
26229
26252
|
this.error("layout ordered applies to the whole diagram; write it at the top level", this.locOf(keyword, line));
|
|
26230
26253
|
return;
|
|
26231
26254
|
}
|
|
26232
|
-
if (this.file !==
|
|
26255
|
+
if (this.file !== this.rootFile) return;
|
|
26233
26256
|
if (this.layoutLocs.ordered) {
|
|
26234
26257
|
this.warning(`layout ordered is already set ${this.where(this.layoutLocs.ordered, loc)}`, this.locOf(keyword, line));
|
|
26235
26258
|
return;
|
|
@@ -26279,21 +26302,13 @@ var Parser = class {
|
|
|
26279
26302
|
const loc = this.locOf(pathToken, line);
|
|
26280
26303
|
const entry = { path, loc };
|
|
26281
26304
|
this.imports.push(entry);
|
|
26282
|
-
const { resolve:
|
|
26283
|
-
if (!
|
|
26305
|
+
const { resolve: resolve3 } = this.options;
|
|
26306
|
+
if (!resolve3) {
|
|
26284
26307
|
this.warning(`Imports are not resolved in this context; '${path}' was not loaded`, loc);
|
|
26285
26308
|
return;
|
|
26286
26309
|
}
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
target = resolve4(path, this.file ?? this.options.path);
|
|
26290
|
-
} catch {
|
|
26291
|
-
target = void 0;
|
|
26292
|
-
}
|
|
26293
|
-
if (!target) {
|
|
26294
|
-
this.error(`Cannot find '${path}'`, loc);
|
|
26295
|
-
return;
|
|
26296
|
-
}
|
|
26310
|
+
const target = this.resolveFile(path, loc);
|
|
26311
|
+
if (!target) return;
|
|
26297
26312
|
entry.resolved = target.path;
|
|
26298
26313
|
const cycleStart = this.chain.indexOf(target.path);
|
|
26299
26314
|
if (cycleStart !== -1) {
|
|
@@ -26313,6 +26328,25 @@ var Parser = class {
|
|
|
26313
26328
|
this.chain.pop();
|
|
26314
26329
|
({ lines: this.lines, file: this.file, stack: this.stack } = outer);
|
|
26315
26330
|
}
|
|
26331
|
+
/**
|
|
26332
|
+
* Loads `path` through the resolver, relative to the file being read. A
|
|
26333
|
+
* resolver error named `ImportError` (outside the project root, not synced,
|
|
26334
|
+
* …) is reported with its own message; anything else is "Cannot find".
|
|
26335
|
+
*/
|
|
26336
|
+
resolveFile(path, loc) {
|
|
26337
|
+
let target;
|
|
26338
|
+
try {
|
|
26339
|
+
target = this.options.resolve?.(path, this.file ?? this.options.path);
|
|
26340
|
+
} catch (e) {
|
|
26341
|
+
if (e instanceof Error && e.name === "ImportError") {
|
|
26342
|
+
this.error(e.message, loc);
|
|
26343
|
+
return void 0;
|
|
26344
|
+
}
|
|
26345
|
+
target = void 0;
|
|
26346
|
+
}
|
|
26347
|
+
if (!target) this.error(`Cannot find '${path}'`, loc);
|
|
26348
|
+
return target;
|
|
26349
|
+
}
|
|
26316
26350
|
parseGroup(tokens, line) {
|
|
26317
26351
|
const keyword = tokens[0];
|
|
26318
26352
|
const top = this.top();
|
|
@@ -26372,6 +26406,273 @@ var Parser = class {
|
|
|
26372
26406
|
this.addNode(group);
|
|
26373
26407
|
this.stack.push({ kind: "group", id: idToken.value, loc });
|
|
26374
26408
|
}
|
|
26409
|
+
// ---- Systems: a team's design behind a contract (docs/design/team-systems-and-imports.md) ----
|
|
26410
|
+
/** `system id ["Name"] [@team] [pos x,y] [color c] [.tag] [rank N] {` */
|
|
26411
|
+
parseSystem(tokens, line) {
|
|
26412
|
+
const [keyword, idToken] = tokens;
|
|
26413
|
+
const top = this.top();
|
|
26414
|
+
if (top && top.kind !== "group") {
|
|
26415
|
+
this.error("A system is declared at the top level or inside a group", this.locOf(keyword, line));
|
|
26416
|
+
this.skipSection("test", tokens, line);
|
|
26417
|
+
return;
|
|
26418
|
+
}
|
|
26419
|
+
const loc = this.locOf(idToken, line);
|
|
26420
|
+
const node = { id: idToken.value, kind: "system", name: idToken.value, type: "service", techStack: SYSTEM_TECH, parent: this.currentGroup(), loc };
|
|
26421
|
+
const style = {};
|
|
26422
|
+
let named = false;
|
|
26423
|
+
let i = 2;
|
|
26424
|
+
while (i < tokens.length - 1) {
|
|
26425
|
+
const t = tokens[i];
|
|
26426
|
+
const styleUsed = this.readStylePart(tokens, i, line, style, false);
|
|
26427
|
+
const used2 = styleUsed === 0 ? this.readRank(tokens, i, line, node) : styleUsed;
|
|
26428
|
+
if (used2 === void 0) return this.skipSection("test", tokens, line);
|
|
26429
|
+
if (used2 > 0) {
|
|
26430
|
+
i += used2;
|
|
26431
|
+
continue;
|
|
26432
|
+
}
|
|
26433
|
+
if (t.kind === "string" && !named) {
|
|
26434
|
+
node.name = t.value;
|
|
26435
|
+
named = true;
|
|
26436
|
+
} else if (t.kind === "string" && node.description === void 0) {
|
|
26437
|
+
node.description = t.value;
|
|
26438
|
+
} else if (t.kind === "team") {
|
|
26439
|
+
node.ownerTeam = t.value;
|
|
26440
|
+
} else if (isWord(t, "pos")) {
|
|
26441
|
+
const [x, comma, y] = [tokens[i + 1], tokens[i + 2], tokens[i + 3]];
|
|
26442
|
+
if (x?.kind !== "number" || comma?.kind !== "comma" || y?.kind !== "number") {
|
|
26443
|
+
this.error("Expected pos x,y", this.locOf(t, line));
|
|
26444
|
+
return this.skipSection("test", tokens, line);
|
|
26445
|
+
}
|
|
26446
|
+
node.position = { x: Number(x.value), y: Number(y.value) };
|
|
26447
|
+
i += 3;
|
|
26448
|
+
} else {
|
|
26449
|
+
this.error(`Unexpected ${describe(t)} in system header; write system id "Name" @Team {`, this.locOf(t, line));
|
|
26450
|
+
return this.skipSection("test", tokens, line);
|
|
26451
|
+
}
|
|
26452
|
+
i++;
|
|
26453
|
+
}
|
|
26454
|
+
this.applyStyle(node, style);
|
|
26455
|
+
const system = { id: node.id, entries: [], loc };
|
|
26456
|
+
if (!this.nodes.has(node.id)) {
|
|
26457
|
+
this.systems.push(system);
|
|
26458
|
+
this.systemById.set(system.id, system);
|
|
26459
|
+
}
|
|
26460
|
+
this.addNode(node);
|
|
26461
|
+
this.stack.push({ kind: "system", system, useCaseLocs: /* @__PURE__ */ new Map(), loc });
|
|
26462
|
+
}
|
|
26463
|
+
/** A line inside `system … { }` or its multi-line `capacity { }`. */
|
|
26464
|
+
parseSystemLine(frame, tokens, line) {
|
|
26465
|
+
const [first, second] = tokens;
|
|
26466
|
+
const { system } = frame;
|
|
26467
|
+
if (first.kind === "rbrace") {
|
|
26468
|
+
this.stack.pop();
|
|
26469
|
+
if (frame.kind === "system") this.finishSystem(frame);
|
|
26470
|
+
if (second) this.error("Unexpected input after }", this.locOf(second, line));
|
|
26471
|
+
return;
|
|
26472
|
+
}
|
|
26473
|
+
if (frame.kind === "stub") {
|
|
26474
|
+
this.readStub(system.stub, tokens, 0, tokens.length, line);
|
|
26475
|
+
return;
|
|
26476
|
+
}
|
|
26477
|
+
if (isWord(first, "from")) {
|
|
26478
|
+
if (second?.kind !== "string" || !second.value.trim()) {
|
|
26479
|
+
this.error('Expected the team file, e.g. from "teams/orders.proschi"', this.locOf(second ?? first, line));
|
|
26480
|
+
return;
|
|
26481
|
+
}
|
|
26482
|
+
if (system.from !== void 0) {
|
|
26483
|
+
this.error(`System '${system.id}' already has from "${system.from}"`, this.locOf(first, line));
|
|
26484
|
+
return;
|
|
26485
|
+
}
|
|
26486
|
+
this.expectEnd(tokens, 2, line);
|
|
26487
|
+
system.from = second.value;
|
|
26488
|
+
frame.fromLoc = this.locOf(second, line);
|
|
26489
|
+
return;
|
|
26490
|
+
}
|
|
26491
|
+
if (isWord(first, "expose")) return this.parseExpose(frame, tokens, line);
|
|
26492
|
+
if (isWord(first, "capacity") && second?.kind === "lbrace") {
|
|
26493
|
+
const opens = tokens[tokens.length - 1].kind !== "rbrace";
|
|
26494
|
+
if (system.stub) {
|
|
26495
|
+
this.error(`System '${system.id}' already has a capacity`, this.locOf(first, line));
|
|
26496
|
+
if (opens) this.stack.push({ kind: "section", section: "capacity", discard: true, loc: this.locOf(first, line) });
|
|
26497
|
+
return;
|
|
26498
|
+
}
|
|
26499
|
+
const stub = { loc: this.lineLoc(tokens, line) };
|
|
26500
|
+
system.stub = stub;
|
|
26501
|
+
this.readStub(stub, tokens, 2, opens ? tokens.length : tokens.length - 1, line);
|
|
26502
|
+
if (opens) this.stack.push({ kind: "stub", system, loc: this.locOf(first, line) });
|
|
26503
|
+
return;
|
|
26504
|
+
}
|
|
26505
|
+
this.error('Inside a system, write from "file", expose <id> "Name" -> "Use case" or capacity { \u2026 }', this.locOf(first, line));
|
|
26506
|
+
if (tokens[tokens.length - 1].kind === "lbrace") this.stack.push({ kind: "section", section: "test", discard: true, loc: this.locOf(first, line) });
|
|
26507
|
+
}
|
|
26508
|
+
/** `expose id ["Name"] [-> "Use case"]` */
|
|
26509
|
+
parseExpose(frame, tokens, line) {
|
|
26510
|
+
const [keyword, idToken] = tokens;
|
|
26511
|
+
if (idToken?.kind !== "ident") {
|
|
26512
|
+
this.error('Expected an entry point id, e.g. expose api "Orders API" -> "Place order"', this.locOf(idToken ?? keyword, line));
|
|
26513
|
+
return;
|
|
26514
|
+
}
|
|
26515
|
+
const entry = { id: idToken.value, name: idToken.value, loc: this.locOf(idToken, line) };
|
|
26516
|
+
let i = 2;
|
|
26517
|
+
if (tokens[i]?.kind === "string") entry.name = tokens[i++].value;
|
|
26518
|
+
if (tokens[i]?.kind === "arrow") {
|
|
26519
|
+
const useCase = tokens[i + 1];
|
|
26520
|
+
if (tokens[i].value !== "->" || useCase?.kind !== "string" && useCase?.kind !== "ident") {
|
|
26521
|
+
this.error('Expected -> "Use case" after the entry point', this.locOf(useCase ?? tokens[i], line));
|
|
26522
|
+
return;
|
|
26523
|
+
}
|
|
26524
|
+
entry.useCase = useCase.value;
|
|
26525
|
+
frame.useCaseLocs.set(entry, this.locOf(useCase, line));
|
|
26526
|
+
i += 2;
|
|
26527
|
+
}
|
|
26528
|
+
if (!this.expectEnd(tokens, i, line)) return;
|
|
26529
|
+
if (frame.system.entries.some((e) => e.id === entry.id)) {
|
|
26530
|
+
this.error(`System '${frame.system.id}' already exposes '${entry.id}'`, entry.loc);
|
|
26531
|
+
return;
|
|
26532
|
+
}
|
|
26533
|
+
frame.system.entries.push(entry);
|
|
26534
|
+
}
|
|
26535
|
+
/** The parts of a system's `capacity { … }` in `tokens[from, to)`. */
|
|
26536
|
+
readStub(stub, tokens, from, to, line) {
|
|
26537
|
+
for (let i = from; i < to; ) {
|
|
26538
|
+
const t = tokens[i];
|
|
26539
|
+
if (t.kind === "quantity" || t.kind === "number") {
|
|
26540
|
+
const rps = this.quantityOf(t, "rps", t, line);
|
|
26541
|
+
if (rps === void 0) return;
|
|
26542
|
+
stub.rps = rps;
|
|
26543
|
+
i++;
|
|
26544
|
+
} else if (isWord(t, "p99") || isWord(t, "latency") || isWord(t, "availability")) {
|
|
26545
|
+
const value = this.quantityOf(i + 1 < to ? tokens[i + 1] : void 0, t.value === "availability" ? "%" : "ms", t, line);
|
|
26546
|
+
if (value === void 0) return;
|
|
26547
|
+
if (t.value === "p99") stub.p99Ms = value;
|
|
26548
|
+
else if (t.value === "latency") stub.latencyMs = value;
|
|
26549
|
+
else stub.availability = value;
|
|
26550
|
+
i += 2;
|
|
26551
|
+
} else {
|
|
26552
|
+
this.error(`Expected ${STUB_HELP}`, this.locOf(t, line));
|
|
26553
|
+
return;
|
|
26554
|
+
}
|
|
26555
|
+
}
|
|
26556
|
+
}
|
|
26557
|
+
/** At the system's `}`: loads the team file and checks the entry points against its use cases. */
|
|
26558
|
+
finishSystem(frame) {
|
|
26559
|
+
const { system } = frame;
|
|
26560
|
+
if (system.from !== void 0) {
|
|
26561
|
+
const team2 = this.loadTeam(system.from, frame.fromLoc ?? system.loc);
|
|
26562
|
+
if (team2) {
|
|
26563
|
+
system.resolved = team2.path;
|
|
26564
|
+
system.diagram = team2.diagram;
|
|
26565
|
+
}
|
|
26566
|
+
if (system.stub) {
|
|
26567
|
+
this.warning(`System '${system.id}' has a from file, which sets its contract; this capacity is ignored`, system.stub.loc);
|
|
26568
|
+
if (team2) delete system.stub;
|
|
26569
|
+
}
|
|
26570
|
+
} else if (!system.stub) {
|
|
26571
|
+
this.warning(`System '${system.id}' has no from "file" and no capacity { \u2026 }; it is drawn but not simulated`, system.loc);
|
|
26572
|
+
}
|
|
26573
|
+
const team = system.diagram;
|
|
26574
|
+
if (!team) return;
|
|
26575
|
+
const name = fileName(system.resolved ?? "");
|
|
26576
|
+
for (const entry of system.entries) {
|
|
26577
|
+
const wanted = entry.useCase;
|
|
26578
|
+
if (wanted !== void 0) {
|
|
26579
|
+
const u = findUseCaseIn(team, wanted);
|
|
26580
|
+
if (u) entry.useCaseId = u.id;
|
|
26581
|
+
else {
|
|
26582
|
+
const near = team.useCases.find((x) => editDistance2(x.name.toLowerCase(), wanted.toLowerCase()) <= Math.max(1, Math.floor(wanted.length / 4)));
|
|
26583
|
+
const all = team.useCases.map((x) => `"${x.name}"`).join(", ");
|
|
26584
|
+
const help = near ? ` Did you mean "${near.name}"?` : all ? ` Its use cases are ${all}` : "";
|
|
26585
|
+
this.error(`${name} has no use case "${wanted}".${help}`, frame.useCaseLocs.get(entry) ?? entry.loc);
|
|
26586
|
+
}
|
|
26587
|
+
} else if (team.useCases.length === 1) {
|
|
26588
|
+
entry.useCaseId = team.useCases[0].id;
|
|
26589
|
+
} else if (team.useCases.length === 0) {
|
|
26590
|
+
this.error(`${name} has no use case for '${entry.id}' to enter`, entry.loc);
|
|
26591
|
+
} else {
|
|
26592
|
+
this.error(`Say which use case '${entry.id}' enters, e.g. expose ${entry.id} -> "${team.useCases[0].name}"`, entry.loc);
|
|
26593
|
+
}
|
|
26594
|
+
}
|
|
26595
|
+
if (system.entries.length === 0) this.warning(`System '${system.id}' exposes no entry point; add expose <id> -> "Use case" so others can call it`, system.loc);
|
|
26596
|
+
}
|
|
26597
|
+
/** Parses a team file in its own scope (once per root parse), through the import resolver. */
|
|
26598
|
+
loadTeam(path, loc) {
|
|
26599
|
+
if (!this.options.resolve) {
|
|
26600
|
+
this.warning(`Files are not resolved in this context; '${path}' was not loaded`, loc);
|
|
26601
|
+
return void 0;
|
|
26602
|
+
}
|
|
26603
|
+
const target = this.resolveFile(path, loc);
|
|
26604
|
+
if (!target) return void 0;
|
|
26605
|
+
const cycleStart = this.chain.indexOf(target.path);
|
|
26606
|
+
if (cycleStart !== -1) {
|
|
26607
|
+
const names = [...this.chain.slice(cycleStart), target.path].map((p) => fileName(p ?? ""));
|
|
26608
|
+
this.error(`System cycle: ${names.join(" \u2192 ")}`, loc);
|
|
26609
|
+
return void 0;
|
|
26610
|
+
}
|
|
26611
|
+
let result = this.teamFiles.get(target.path);
|
|
26612
|
+
if (!result) {
|
|
26613
|
+
const scope2 = { chain: [...this.chain, target.path], teamFiles: this.teamFiles };
|
|
26614
|
+
result = new _Parser(target.source, { path: target.path, resolve: this.options.resolve }, scope2).run();
|
|
26615
|
+
this.teamFiles.set(target.path, result);
|
|
26616
|
+
for (const d of result.diagnostics) {
|
|
26617
|
+
if (!this.fileOrder.has(d.file)) this.fileOrder.set(d.file, this.fileOrder.size);
|
|
26618
|
+
this.diagnostics.push(d);
|
|
26619
|
+
}
|
|
26620
|
+
}
|
|
26621
|
+
return { path: target.path, diagram: result.diagram };
|
|
26622
|
+
}
|
|
26623
|
+
/**
|
|
26624
|
+
* Resolves `orders.api` references once every file is read: the entry point
|
|
26625
|
+
* `api` of system `orders`. Edges and references then name the system, with
|
|
26626
|
+
* the entry point in `sourcePort` / `targetPort`. On a connection to a
|
|
26627
|
+
* non-system, `b.planned` stays what it always was: `b` with a tag.
|
|
26628
|
+
*/
|
|
26629
|
+
resolvePorts() {
|
|
26630
|
+
for (const ref of this.references) {
|
|
26631
|
+
const dot = ref.id.indexOf(".");
|
|
26632
|
+
if (dot === -1) continue;
|
|
26633
|
+
const prefix = ref.id.slice(0, dot);
|
|
26634
|
+
const port = ref.id.slice(dot + 1);
|
|
26635
|
+
const system = this.systemById.get(prefix);
|
|
26636
|
+
if (system) {
|
|
26637
|
+
if (!system.entries.some((e) => e.id === port)) this.error(this.unexposedMessage(system, port), ref.loc);
|
|
26638
|
+
} else if (ref.edgeTarget) {
|
|
26639
|
+
ref.edgeTarget.tag = port;
|
|
26640
|
+
this.tagRefs.push({ tag: port, loc: { ...ref.loc, col: ref.loc.col + dot, length: ref.loc.length - dot }, target: ref.edgeTarget });
|
|
26641
|
+
} else {
|
|
26642
|
+
this.error(`'${prefix}' is not a system, so '${ref.id}' names nothing; write ${prefix}, or declare system ${prefix} { \u2026 }`, ref.loc);
|
|
26643
|
+
}
|
|
26644
|
+
ref.id = prefix;
|
|
26645
|
+
}
|
|
26646
|
+
for (const edge of this.edges) {
|
|
26647
|
+
const [source2, sourcePort] = this.splitRef(edge.source);
|
|
26648
|
+
const [target, targetPort] = this.splitRef(edge.target);
|
|
26649
|
+
edge.source = source2;
|
|
26650
|
+
edge.target = target;
|
|
26651
|
+
if (sourcePort !== void 0) edge.sourcePort = sourcePort;
|
|
26652
|
+
if (targetPort !== void 0) edge.targetPort = targetPort;
|
|
26653
|
+
}
|
|
26654
|
+
}
|
|
26655
|
+
unexposedMessage(system, port) {
|
|
26656
|
+
const entries2 = system.entries.map((e) => `${system.id}.${e.id}`);
|
|
26657
|
+
const known = entries2.length ? `; connect to ${entries2.join(" or ")}` : "";
|
|
26658
|
+
const internal = system.diagram?.nodes.some((n) => n.id === port);
|
|
26659
|
+
return internal ? `'${port}' is inside system '${system.id}' but is not an entry point; only entry points are reachable from outside: add expose ${port} -> "Use case"${known}` : `System '${system.id}' has no entry point '${port}'${known}`;
|
|
26660
|
+
}
|
|
26661
|
+
/** `orders.api` → `['orders', 'api']` when `orders` is a system; else the id up to its dot, without a port. */
|
|
26662
|
+
splitRef(id) {
|
|
26663
|
+
const dot = id.indexOf(".");
|
|
26664
|
+
if (dot === -1) return [id, void 0];
|
|
26665
|
+
const prefix = id.slice(0, dot);
|
|
26666
|
+
return [prefix, this.systemById.has(prefix) ? id.slice(dot + 1) : void 0];
|
|
26667
|
+
}
|
|
26668
|
+
/** A step to a whole system enters its only entry point; with several it must name one. */
|
|
26669
|
+
onlyEntry(id, loc) {
|
|
26670
|
+
const system = this.systemById.get(id);
|
|
26671
|
+
if (!system || system.entries.length === 0) return void 0;
|
|
26672
|
+
if (system.entries.length === 1) return system.entries[0].id;
|
|
26673
|
+
this.errorOnce(`Say which entry point of '${id}' this step enters: ${system.entries.map((e) => `${id}.${e.id}`).join(" or ")}`, loc);
|
|
26674
|
+
return void 0;
|
|
26675
|
+
}
|
|
26375
26676
|
parseUseCase(tokens, line) {
|
|
26376
26677
|
const keyword = tokens[0];
|
|
26377
26678
|
if (this.stack.length > 0) {
|
|
@@ -26561,7 +26862,8 @@ var Parser = class {
|
|
|
26561
26862
|
const from = fromToken.value;
|
|
26562
26863
|
const to = toToken.value;
|
|
26563
26864
|
const arrow = arrowToken.value;
|
|
26564
|
-
|
|
26865
|
+
const toRef = { id: to, loc: this.locOf(toToken, line) };
|
|
26866
|
+
this.references.push({ id: from, loc: this.locOf(fromToken, line) }, toRef);
|
|
26565
26867
|
const top = this.top();
|
|
26566
26868
|
const inUseCase = top?.kind === "usecase" || top?.kind === "alt" || top?.kind === "par";
|
|
26567
26869
|
if (tagToken && inUseCase) {
|
|
@@ -26596,6 +26898,7 @@ var Parser = class {
|
|
|
26596
26898
|
edge.tag = tagToken.value;
|
|
26597
26899
|
this.tagRefs.push({ tag: tagToken.value, loc: this.locOf(tagToken, line), target: edge });
|
|
26598
26900
|
}
|
|
26901
|
+
if (!tagToken) toRef.edgeTarget = edge;
|
|
26599
26902
|
this.edges.push(edge);
|
|
26600
26903
|
}
|
|
26601
26904
|
return lastLine;
|
|
@@ -27506,7 +27809,9 @@ var Parser = class {
|
|
|
27506
27809
|
for (const step of raw) {
|
|
27507
27810
|
this.checkConnection(step);
|
|
27508
27811
|
if (step.arrow === "-->") {
|
|
27509
|
-
const
|
|
27812
|
+
const [respFrom] = this.splitRef(step.from);
|
|
27813
|
+
const [respTo] = this.splitRef(step.to);
|
|
27814
|
+
const request2 = [...steps].reverse().find((s) => s.fromServiceId === respTo && s.toServiceId === respFrom && !answered.has(s) && !s.failed);
|
|
27510
27815
|
if (!request2) {
|
|
27511
27816
|
this.warning(`Response has no matching request from '${step.to}' to '${step.from}'`, step.loc);
|
|
27512
27817
|
continue;
|
|
@@ -27525,14 +27830,17 @@ var Parser = class {
|
|
|
27525
27830
|
continue;
|
|
27526
27831
|
}
|
|
27527
27832
|
const label = parseStepLabel(step.label);
|
|
27528
|
-
const
|
|
27833
|
+
const [from, fromPort] = this.splitRef(step.from);
|
|
27834
|
+
const [to, toPort] = this.splitRef(step.to);
|
|
27835
|
+
const port = toPort ?? this.onlyEntry(to, step.loc);
|
|
27836
|
+
const target = this.nodes.get(to);
|
|
27529
27837
|
const isAsync = step.arrow === "->>";
|
|
27530
27838
|
steps.push({
|
|
27531
27839
|
id: `${prefix}-${steps.length + 1}`,
|
|
27532
27840
|
stepOrder: steps.length,
|
|
27533
27841
|
stepName: label.name || `${step.from} \u2192 ${step.to}`,
|
|
27534
|
-
fromServiceId:
|
|
27535
|
-
toServiceId:
|
|
27842
|
+
fromServiceId: from,
|
|
27843
|
+
toServiceId: to,
|
|
27536
27844
|
protocol: inferProtocol(isAsync, label.method, target),
|
|
27537
27845
|
httpMethod: label.method ?? "",
|
|
27538
27846
|
endpoint: label.endpoint ?? "",
|
|
@@ -27548,7 +27856,9 @@ var Parser = class {
|
|
|
27548
27856
|
loc: step.loc,
|
|
27549
27857
|
...step.multiplier !== void 0 ? { multiplier: step.multiplier } : {},
|
|
27550
27858
|
...step.sizeBytes !== void 0 ? { sizeBytes: step.sizeBytes } : {},
|
|
27551
|
-
access: accessOf(label.method, step.label)
|
|
27859
|
+
access: accessOf(label.method, step.label),
|
|
27860
|
+
...port !== void 0 ? { toPort: port } : {},
|
|
27861
|
+
...fromPort !== void 0 ? { fromPort } : {}
|
|
27552
27862
|
});
|
|
27553
27863
|
}
|
|
27554
27864
|
return steps;
|
|
@@ -27558,8 +27868,10 @@ var Parser = class {
|
|
|
27558
27868
|
* Documents without any connection are use-case-only sketches; they are left alone.
|
|
27559
27869
|
*/
|
|
27560
27870
|
checkConnection(step) {
|
|
27561
|
-
|
|
27562
|
-
const [
|
|
27871
|
+
const [a] = this.splitRef(step.from);
|
|
27872
|
+
const [b] = this.splitRef(step.to);
|
|
27873
|
+
if (this.edges.length === 0 || a === b || this.connected.has(`${a}|${b}`)) return;
|
|
27874
|
+
const [from, to] = step.arrow === "-->" ? [b, a] : [a, b];
|
|
27563
27875
|
this.warning(`No connection between '${from}' and '${to}' in the architecture; add '${from} -> ${to}'`, step.loc);
|
|
27564
27876
|
}
|
|
27565
27877
|
createImplicitNodes() {
|
|
@@ -27655,6 +27967,17 @@ var Parser = class {
|
|
|
27655
27967
|
error(message, loc) {
|
|
27656
27968
|
this.diagnostics.push({ severity: "error", message, ...loc });
|
|
27657
27969
|
}
|
|
27970
|
+
/** An error for a step that every scenario sharing it would report again. */
|
|
27971
|
+
errorOnce(message, loc) {
|
|
27972
|
+
const key = `error
|
|
27973
|
+
${loc.file ?? ""}
|
|
27974
|
+
${loc.line}
|
|
27975
|
+
${loc.col}
|
|
27976
|
+
${message}`;
|
|
27977
|
+
if (this.warned.has(key)) return;
|
|
27978
|
+
this.warned.add(key);
|
|
27979
|
+
this.error(message, loc);
|
|
27980
|
+
}
|
|
27658
27981
|
warning(message, loc) {
|
|
27659
27982
|
const key = `${loc.file ?? ""}
|
|
27660
27983
|
${loc.line}
|
|
@@ -27690,6 +28013,23 @@ function expand(items) {
|
|
|
27690
28013
|
}
|
|
27691
28014
|
return paths;
|
|
27692
28015
|
}
|
|
28016
|
+
function qualify(tokens) {
|
|
28017
|
+
let out;
|
|
28018
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
28019
|
+
const [t, next] = [tokens[i], tokens[i + 1]];
|
|
28020
|
+
const joined = t.kind === "ident" && next?.kind === "tag" && next.col === t.col + t.length;
|
|
28021
|
+
if (joined && (tokens[i + 2]?.kind === "arrow" || tokens[i - 1]?.kind === "arrow")) {
|
|
28022
|
+
out ??= tokens.slice(0, i);
|
|
28023
|
+
out.push({ ...t, value: `${t.value}.${next.value}`, length: t.length + next.length });
|
|
28024
|
+
i++;
|
|
28025
|
+
} else out?.push(t);
|
|
28026
|
+
}
|
|
28027
|
+
return out ?? tokens;
|
|
28028
|
+
}
|
|
28029
|
+
function findUseCaseIn(diagram, name) {
|
|
28030
|
+
const lower = name.toLowerCase();
|
|
28031
|
+
return diagram.useCases.find((u) => u.name === name) ?? diagram.useCases.find((u) => u.name.toLowerCase() === lower) ?? diagram.useCases.find((u) => u.id === name);
|
|
28032
|
+
}
|
|
27693
28033
|
function isWord(token, word) {
|
|
27694
28034
|
return token?.kind === "ident" && token.value === word;
|
|
27695
28035
|
}
|
|
@@ -28489,6 +28829,340 @@ function formatUsd(usd) {
|
|
|
28489
28829
|
return `$${Math.round(usd).toLocaleString("en-US")}/month`;
|
|
28490
28830
|
}
|
|
28491
28831
|
|
|
28832
|
+
// ../frontend/src/sim/overlay.ts
|
|
28833
|
+
function flowsOf(diagram, traffic) {
|
|
28834
|
+
const flows = /* @__PURE__ */ new Map();
|
|
28835
|
+
for (const t of traffic) {
|
|
28836
|
+
const u = findUseCase(diagram, t.useCase);
|
|
28837
|
+
if (!u || t.rps <= 0) continue;
|
|
28838
|
+
for (const sc of u.scenarios) {
|
|
28839
|
+
const share = t.mix ? t.mix.find((m) => m.scenario === sc.name)?.share ?? 0 : sc === u.scenarios[0] ? 1 : 0;
|
|
28840
|
+
if (share <= 0) continue;
|
|
28841
|
+
for (const step of sc.steps) {
|
|
28842
|
+
if (step.failed) continue;
|
|
28843
|
+
const key = `${step.fromServiceId}>${step.toServiceId}`;
|
|
28844
|
+
const f = flows.get(key) ?? { from: step.fromServiceId, to: step.toServiceId, rps: 0, async: step.executionType !== "SYNC_REQUEST_RESPONSE" };
|
|
28845
|
+
f.rps += t.rps * share * (step.multiplier ?? 1);
|
|
28846
|
+
flows.set(key, f);
|
|
28847
|
+
}
|
|
28848
|
+
}
|
|
28849
|
+
}
|
|
28850
|
+
return [...flows.values()];
|
|
28851
|
+
}
|
|
28852
|
+
function analysisOverlay(diagram, analysis) {
|
|
28853
|
+
const nodes = {};
|
|
28854
|
+
for (const n of analysis.nodes) {
|
|
28855
|
+
const works = Number.isFinite(n.capacityRps) && n.kind !== "client";
|
|
28856
|
+
nodes[n.id] = {
|
|
28857
|
+
...works ? { utilization: n.utilization, saturated: n.saturated } : {},
|
|
28858
|
+
...n.replicas > 1 ? { replicas: n.replicas } : {},
|
|
28859
|
+
...n.shards > 1 ? { shards: n.shards } : {}
|
|
28860
|
+
};
|
|
28861
|
+
}
|
|
28862
|
+
return { nodes, flows: flowsOf(diagram, diagram.traffic ?? []) };
|
|
28863
|
+
}
|
|
28864
|
+
|
|
28865
|
+
// ../frontend/src/sim/systems.ts
|
|
28866
|
+
var teamKey = (s) => s.resolved ?? s.diagram;
|
|
28867
|
+
var contracts = /* @__PURE__ */ new WeakMap();
|
|
28868
|
+
function contractOf(system, options = {}) {
|
|
28869
|
+
let contract = contracts.get(system);
|
|
28870
|
+
if (!contract) {
|
|
28871
|
+
contract = system.diagram ? designContract(system, system.diagram, options) : stubContract(system);
|
|
28872
|
+
contracts.set(system, contract);
|
|
28873
|
+
}
|
|
28874
|
+
return contract;
|
|
28875
|
+
}
|
|
28876
|
+
function stubContract(system) {
|
|
28877
|
+
const stub = system.stub;
|
|
28878
|
+
if (!stub) return { source: "none", entries: system.entries.map((e) => emptyEntry(e.id, e.name)) };
|
|
28879
|
+
const rps = stub.rps ?? Infinity;
|
|
28880
|
+
return {
|
|
28881
|
+
source: "stub",
|
|
28882
|
+
entries: system.entries.map((e) => ({
|
|
28883
|
+
...emptyEntry(e.id, e.name),
|
|
28884
|
+
latency: stub.p99Ms !== void 0 ? [{ percentile: 99, maxMs: stub.p99Ms }] : [],
|
|
28885
|
+
...stub.availability !== void 0 ? { availabilityPercent: stub.availability } : {},
|
|
28886
|
+
saturatesAtRps: rps,
|
|
28887
|
+
hotAtRps: rps * HOT
|
|
28888
|
+
}))
|
|
28889
|
+
};
|
|
28890
|
+
}
|
|
28891
|
+
var emptyEntry = (entry, name) => ({ entry, name, ownRps: 0, latency: [], saturatesAtRps: Infinity, hotAtRps: Infinity });
|
|
28892
|
+
function designContract(system, team, options) {
|
|
28893
|
+
const traffic = resolveTraffic(team);
|
|
28894
|
+
const base = computeLoad(team, traffic);
|
|
28895
|
+
const baseBytes = computeBytesPerSecond(team, traffic);
|
|
28896
|
+
const profiles = profilesOf(team);
|
|
28897
|
+
const nodes = components(team);
|
|
28898
|
+
const entries2 = system.entries.map((e) => {
|
|
28899
|
+
const u = team.useCases.find((x) => x.id === e.useCaseId);
|
|
28900
|
+
if (!u) return emptyEntry(e.id, e.name);
|
|
28901
|
+
const own = traffic.get(u.id);
|
|
28902
|
+
const unit = /* @__PURE__ */ new Map([[u.id, { rps: 1, shares: own.shares }]]);
|
|
28903
|
+
const delta = computeLoad(team, unit);
|
|
28904
|
+
const deltaBytes = computeBytesPerSecond(team, unit);
|
|
28905
|
+
const result = { ...emptyEntry(e.id, e.name), useCase: u.name, ownRps: own.rps };
|
|
28906
|
+
for (const n of nodes) {
|
|
28907
|
+
const d = delta.get(n.id);
|
|
28908
|
+
const db = deltaBytes.get(n.id) ?? 0;
|
|
28909
|
+
if (!d && !db) continue;
|
|
28910
|
+
const p = profiles.get(n.id);
|
|
28911
|
+
const replicas = replicasOf(n);
|
|
28912
|
+
const b = base.get(n.id) ?? { read: 0, write: 0 };
|
|
28913
|
+
const bb = baseBytes.get(n.id) ?? 0;
|
|
28914
|
+
const u0 = usageOf(p, replicas, b.read, b.write, bb);
|
|
28915
|
+
const u1 = usageOf(p, replicas, b.read + (d?.read ?? 0), b.write + (d?.write ?? 0), bb + db);
|
|
28916
|
+
const sat = crossing(u0, u1, 1);
|
|
28917
|
+
const hot = crossing(u0, u1, HOT);
|
|
28918
|
+
if (sat < result.saturatesAtRps) {
|
|
28919
|
+
result.saturatesAtRps = sat;
|
|
28920
|
+
result.bottleneck = n.id;
|
|
28921
|
+
}
|
|
28922
|
+
if (hot < result.hotAtRps) {
|
|
28923
|
+
result.hotAtRps = hot;
|
|
28924
|
+
result.hotNode = n.id;
|
|
28925
|
+
}
|
|
28926
|
+
}
|
|
28927
|
+
for (const r of team.requirements ?? []) {
|
|
28928
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
28929
|
+
if (r.useCase !== void 0 && findUseCase(team, r.useCase) !== u) continue;
|
|
28930
|
+
if (r.kind === "latency") result.latency.push({ percentile: r.percentile, maxMs: r.maxMs, ...r.scenario !== void 0 ? { scenario: r.scenario } : {} });
|
|
28931
|
+
else result.availabilityPercent = Math.max(result.availabilityPercent ?? 0, r.minPercent);
|
|
28932
|
+
}
|
|
28933
|
+
return result;
|
|
28934
|
+
});
|
|
28935
|
+
return { source: "design", entries: entries2, baseline: analyze(team, { timeoutMs: options.timeoutMs }) };
|
|
28936
|
+
}
|
|
28937
|
+
function crossing(u0, u1, theta) {
|
|
28938
|
+
const terms = u0.writeScaling === "shards" ? [
|
|
28939
|
+
[u0.readUtilization, u1.readUtilization],
|
|
28940
|
+
[u0.writeUtilization, u1.writeUtilization]
|
|
28941
|
+
] : [[u0.requestUtilization, u1.requestUtilization]];
|
|
28942
|
+
terms.push([u0.bandwidthUtilization, u1.bandwidthUtilization]);
|
|
28943
|
+
let best = Infinity;
|
|
28944
|
+
for (const [a, b] of terms) {
|
|
28945
|
+
const slope = b - a;
|
|
28946
|
+
if (slope <= 1e-15) continue;
|
|
28947
|
+
if (a >= theta) return 0;
|
|
28948
|
+
best = Math.min(best, (theta - a) / slope);
|
|
28949
|
+
}
|
|
28950
|
+
return best;
|
|
28951
|
+
}
|
|
28952
|
+
function inboundLoad(diagram, traffic) {
|
|
28953
|
+
const result = /* @__PURE__ */ new Map();
|
|
28954
|
+
forEachStepRate(diagram, traffic, (step, rate) => {
|
|
28955
|
+
if (step.toPort === void 0) return;
|
|
28956
|
+
const ports = result.get(step.toServiceId) ?? /* @__PURE__ */ new Map();
|
|
28957
|
+
ports.set(step.toPort, (ports.get(step.toPort) ?? 0) + rate * multiplierOf(step));
|
|
28958
|
+
result.set(step.toServiceId, ports);
|
|
28959
|
+
});
|
|
28960
|
+
return result;
|
|
28961
|
+
}
|
|
28962
|
+
function hopFrom(p50, p99) {
|
|
28963
|
+
const l50 = tailFactor(0.5);
|
|
28964
|
+
const l99 = tailFactor(0.99);
|
|
28965
|
+
let tailMs = Math.max(0, (p99 - p50) / (l99 - l50));
|
|
28966
|
+
let fixedMs = p50 - tailMs * l50;
|
|
28967
|
+
if (fixedMs < 0) {
|
|
28968
|
+
fixedMs = 0;
|
|
28969
|
+
tailMs = p99 / l99;
|
|
28970
|
+
}
|
|
28971
|
+
return { fixedMs, tailMs, waitProbability: 0 };
|
|
28972
|
+
}
|
|
28973
|
+
function analyzeSystems(diagram, traffic, options = {}) {
|
|
28974
|
+
const inbound = inboundLoad(diagram, traffic);
|
|
28975
|
+
if (!options.sharedSystems) options = { ...options, sharedSystems: sharedLoad(diagram, inbound) };
|
|
28976
|
+
const hops = /* @__PURE__ */ new Map();
|
|
28977
|
+
const availabilities = /* @__PURE__ */ new Map();
|
|
28978
|
+
const key = (id, port) => `${id}
|
|
28979
|
+
${port}`;
|
|
28980
|
+
const systems = (diagram.systems ?? []).map((system) => {
|
|
28981
|
+
const result = analyzeSystem(system, inbound.get(system.id) ?? /* @__PURE__ */ new Map(), options);
|
|
28982
|
+
for (const e of result.entries) {
|
|
28983
|
+
hops.set(key(system.id, e.entry), hopFrom(e.p50Ms, e.p99Ms));
|
|
28984
|
+
availabilities.set(key(system.id, e.entry), e.availability);
|
|
28985
|
+
}
|
|
28986
|
+
return result;
|
|
28987
|
+
});
|
|
28988
|
+
return {
|
|
28989
|
+
systems,
|
|
28990
|
+
hop: (id, port) => port === void 0 ? void 0 : hops.get(key(id, port)),
|
|
28991
|
+
availability: (id, port) => port === void 0 ? void 0 : availabilities.get(key(id, port))
|
|
28992
|
+
};
|
|
28993
|
+
}
|
|
28994
|
+
function sentByUseCase(system, sent, into) {
|
|
28995
|
+
let total = 0;
|
|
28996
|
+
for (const e of system.entries) {
|
|
28997
|
+
const rps = sent.get(e.id) ?? 0;
|
|
28998
|
+
if (rps <= 0 || e.useCaseId === void 0) continue;
|
|
28999
|
+
into.set(e.useCaseId, (into.get(e.useCaseId) ?? 0) + rps);
|
|
29000
|
+
total += rps;
|
|
29001
|
+
}
|
|
29002
|
+
return total;
|
|
29003
|
+
}
|
|
29004
|
+
function sharedLoad(root, rootInbound) {
|
|
29005
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
29006
|
+
const designs = /* @__PURE__ */ new Map();
|
|
29007
|
+
const order = [];
|
|
29008
|
+
const visit = (d, prefix) => {
|
|
29009
|
+
for (const s of d.systems ?? []) {
|
|
29010
|
+
if (!s.diagram) continue;
|
|
29011
|
+
const key = teamKey(s);
|
|
29012
|
+
if (designs.has(key)) continue;
|
|
29013
|
+
const path = prefix ? `${prefix}.${s.id}` : s.id;
|
|
29014
|
+
designs.set(key, { diagram: s.diagram, path, loc: s.loc });
|
|
29015
|
+
visit(s.diagram, path);
|
|
29016
|
+
order.push(key);
|
|
29017
|
+
}
|
|
29018
|
+
};
|
|
29019
|
+
visit(root, "");
|
|
29020
|
+
const send = (d, prefix, inbound) => {
|
|
29021
|
+
for (const s of d.systems ?? []) {
|
|
29022
|
+
if (!s.diagram) continue;
|
|
29023
|
+
const key = teamKey(s);
|
|
29024
|
+
const load = byFile.get(key) ?? { useCases: /* @__PURE__ */ new Map(), callers: [] };
|
|
29025
|
+
byFile.set(key, load);
|
|
29026
|
+
const rps = sentByUseCase(s, inbound.get(s.id) ?? /* @__PURE__ */ new Map(), load.useCases);
|
|
29027
|
+
if (rps > 0) load.callers.push({ path: prefix ? `${prefix}.${s.id}` : s.id, ...d.title !== void 0 ? { from: d.title } : {}, rps });
|
|
29028
|
+
}
|
|
29029
|
+
};
|
|
29030
|
+
send(root, "", rootInbound);
|
|
29031
|
+
for (const key of order.reverse()) {
|
|
29032
|
+
const { diagram, path, loc } = designs.get(key);
|
|
29033
|
+
if (!diagram.systems?.length) continue;
|
|
29034
|
+
const combined = withExtraTraffic(diagram, byFile.get(key)?.useCases ?? /* @__PURE__ */ new Map(), loc);
|
|
29035
|
+
send(diagram, path, inboundLoad(combined, resolveTraffic(combined)));
|
|
29036
|
+
}
|
|
29037
|
+
return { byFile, analyses: /* @__PURE__ */ new Map() };
|
|
29038
|
+
}
|
|
29039
|
+
function analyzeSystem(system, sent, options) {
|
|
29040
|
+
const contract = contractOf(system, options);
|
|
29041
|
+
const inbound = system.entries.map((e) => ({ entry: e.id, rps: sent.get(e.id) ?? 0 }));
|
|
29042
|
+
const totalInboundRps = inbound.reduce((sum, e) => sum + e.rps, 0);
|
|
29043
|
+
const result = { id: system.id, contract, inbound, totalInboundRps, utilization: 0, saturated: false, hot: false, breaches: [], entries: [], warnings: [] };
|
|
29044
|
+
const shared = system.diagram ? options.sharedSystems?.byFile.get(teamKey(system)) : void 0;
|
|
29045
|
+
const sending = inbound.filter((e) => e.rps > 0).map((e) => {
|
|
29046
|
+
const c = contract.entries.find((x) => x.entry === e.entry);
|
|
29047
|
+
const useCaseId = system.entries.find((x) => x.id === e.entry)?.useCaseId;
|
|
29048
|
+
const gets = useCaseId !== void 0 ? shared?.useCases.get(useCaseId) ?? 0 : 0;
|
|
29049
|
+
const rps = gets > e.rps * (1 + 1e-9) ? `${formatRps(e.rps).replace(/ rps$/, "")} of the ${formatRps(gets)} it gets` : formatRps(e.rps);
|
|
29050
|
+
return `${rps} into ${c?.useCase ? `"${c.useCase}" (${system.id}.${e.entry})` : `${system.id}.${e.entry}`}`;
|
|
29051
|
+
}).join(" and ");
|
|
29052
|
+
const label = `'${system.id}'`;
|
|
29053
|
+
if (contract.source === "stub") {
|
|
29054
|
+
const stub = system.stub;
|
|
29055
|
+
const rps = stub.rps ?? Infinity;
|
|
29056
|
+
result.utilization = Number.isFinite(rps) && rps > 0 ? totalInboundRps / rps : 0;
|
|
29057
|
+
result.saturated = result.utilization >= 1;
|
|
29058
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
29059
|
+
const base = stub.latencyMs ?? (stub.p99Ms !== void 0 ? stub.p99Ms / (1 - SERVICE_SPREAD + SERVICE_SPREAD * tailFactor(0.99)) : 0);
|
|
29060
|
+
const hop = hopModel(base, 0);
|
|
29061
|
+
const at = (p) => hop.fixedMs + hop.tailMs * tailFactor(p);
|
|
29062
|
+
const availability2 = stub.availability !== void 0 ? stub.availability / 100 : 1;
|
|
29063
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: at(0.5), p99Ms: at(0.99), availability: availability2 }));
|
|
29064
|
+
if (result.saturated) {
|
|
29065
|
+
result.warnings.push(`${label} cannot take what it is sent: you send ${sending}; it takes at most ${formatRps(rps)}.`);
|
|
29066
|
+
if (stub.p99Ms !== void 0) result.breaches.push({ requirement: `p99 < ${formatMs(stub.p99Ms)}`, message: `p99 is not held past ${formatRps(rps)}` });
|
|
29067
|
+
} else if (result.hot) result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}; it runs hot past ${formatRps(rps * HOT)} and takes at most ${formatRps(rps)}.`);
|
|
29068
|
+
return result;
|
|
29069
|
+
}
|
|
29070
|
+
if (contract.source === "none" || !system.diagram) {
|
|
29071
|
+
result.entries = system.entries.map((e) => ({ entry: e.id, p50Ms: 0, p99Ms: 0, availability: 1 }));
|
|
29072
|
+
return result;
|
|
29073
|
+
}
|
|
29074
|
+
const team = system.diagram;
|
|
29075
|
+
const key = teamKey(system);
|
|
29076
|
+
const sentToTeam = shared?.useCases ?? /* @__PURE__ */ new Map();
|
|
29077
|
+
if (!shared) sentByUseCase(system, sent, sentToTeam);
|
|
29078
|
+
const sharedInboundRps = [...sentToTeam.values()].reduce((sum, rps) => sum + rps, 0);
|
|
29079
|
+
result.callers = shared?.callers ?? (totalInboundRps > 0 ? [{ path: system.id, rps: totalInboundRps }] : []);
|
|
29080
|
+
result.sharedInboundRps = sharedInboundRps;
|
|
29081
|
+
let cached = options.sharedSystems?.analyses.get(key);
|
|
29082
|
+
if (!cached) {
|
|
29083
|
+
const combined = sharedInboundRps > 0 ? withExtraTraffic(team, sentToTeam, system.loc) : team;
|
|
29084
|
+
const nested = { timeoutMs: options.timeoutMs, ...options.sharedSystems ? { sharedSystems: options.sharedSystems } : {} };
|
|
29085
|
+
cached = { combined, analysis: sharedInboundRps > 0 ? analyze(combined, nested) : contract.baseline };
|
|
29086
|
+
options.sharedSystems?.analyses.set(key, cached);
|
|
29087
|
+
}
|
|
29088
|
+
const { analysis } = cached;
|
|
29089
|
+
result.analysis = analysis;
|
|
29090
|
+
result.overlay = analysisOverlay(cached.combined, analysis);
|
|
29091
|
+
const busiest = analysis.nodes.reduce((best, n) => !best || n.utilization > best.utilization ? n : best, void 0);
|
|
29092
|
+
result.utilization = busiest?.utilization ?? 0;
|
|
29093
|
+
result.busiest = busiest?.id;
|
|
29094
|
+
result.saturated = !!busiest?.saturated;
|
|
29095
|
+
result.hot = !result.saturated && result.utilization > HOT;
|
|
29096
|
+
const byId = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
29097
|
+
result.entries = system.entries.map((e) => {
|
|
29098
|
+
const u = e.useCaseId !== void 0 ? byId.get(e.useCaseId) : void 0;
|
|
29099
|
+
return { entry: e.id, p50Ms: u?.percentiles.p50 ?? 0, p99Ms: u?.percentiles.p99 ?? 0, availability: u?.availability ?? 1 };
|
|
29100
|
+
});
|
|
29101
|
+
if (totalInboundRps === 0) return result;
|
|
29102
|
+
result.breaches = breachesOf(team, sentToTeam, analysis);
|
|
29103
|
+
const first = contract.entries.find((c) => inbound.some((e) => e.entry === c.entry && e.rps > 0));
|
|
29104
|
+
if (result.saturated && busiest) {
|
|
29105
|
+
const onOwn = first && first.saturatesAtRps === 0;
|
|
29106
|
+
const limit = first && Number.isFinite(first.saturatesAtRps) ? `; it takes at most ${formatRps(first.saturatesAtRps)} before its ${first.bottleneck} saturates` : "";
|
|
29107
|
+
result.warnings.push(
|
|
29108
|
+
onOwn ? `${label} is saturated on its own traffic, before what it is sent: its ${busiest.id} is at ${usageText(busiest)}.` : `${label} cannot take what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`
|
|
29109
|
+
);
|
|
29110
|
+
} else if (result.hot && busiest) {
|
|
29111
|
+
const limit = first && Number.isFinite(first.hotAtRps) ? `; it runs hot past ${formatRps(first.hotAtRps)}` : "";
|
|
29112
|
+
result.warnings.push(`${label} runs hot under what it is sent: you send ${sending}${limit}. Its ${busiest.id} is at ${usageText(busiest)}.`);
|
|
29113
|
+
}
|
|
29114
|
+
for (const b of result.breaches) result.warnings.push(`${label} breaks its SLA under what it is sent: ${b.message}.`);
|
|
29115
|
+
return result;
|
|
29116
|
+
}
|
|
29117
|
+
function withExtraTraffic(team, extra, loc) {
|
|
29118
|
+
const traffic = [...team.traffic ?? []];
|
|
29119
|
+
for (const [id, rps] of extra) {
|
|
29120
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
29121
|
+
if (!u || rps <= 0) continue;
|
|
29122
|
+
const i = traffic.findIndex((t) => findUseCase(team, t.useCase) === u);
|
|
29123
|
+
if (i >= 0) traffic[i] = { ...traffic[i], rps: traffic[i].rps + rps };
|
|
29124
|
+
else traffic.push({ useCase: u.name, rps, loc });
|
|
29125
|
+
}
|
|
29126
|
+
return { ...team, traffic };
|
|
29127
|
+
}
|
|
29128
|
+
function breachesOf(team, sent, analysis) {
|
|
29129
|
+
const entered = /* @__PURE__ */ new Set();
|
|
29130
|
+
for (const [id, rps] of sent) {
|
|
29131
|
+
const u = team.useCases.find((x) => x.id === id);
|
|
29132
|
+
if (u && rps > 0) entered.add(u);
|
|
29133
|
+
}
|
|
29134
|
+
const nodes = new Map(analysis.nodes.map((n) => [n.id, n]));
|
|
29135
|
+
const results = new Map(analysis.useCases.map((u) => [u.id, u]));
|
|
29136
|
+
const breaches = [];
|
|
29137
|
+
for (const r of team.requirements ?? []) {
|
|
29138
|
+
if (r.kind !== "latency" && r.kind !== "availability") continue;
|
|
29139
|
+
const targets = r.useCase === void 0 ? [...entered] : [findUseCase(team, r.useCase)].filter((u) => !!u && entered.has(u));
|
|
29140
|
+
for (const u of targets) {
|
|
29141
|
+
const ua = results.get(u.id);
|
|
29142
|
+
const under = `under ${formatRps(ua.rps)}`;
|
|
29143
|
+
if (r.kind === "availability") {
|
|
29144
|
+
const name2 = `availability of "${u.name}" \u2265 ${r.minPercent}%`;
|
|
29145
|
+
if (ua.availability * 100 < r.minPercent - 1e-9) breaches.push({ requirement: name2, message: `availability of "${u.name}" is ${formatAvailability(ua.availability)} ${under} (limit ${r.minPercent}%)` });
|
|
29146
|
+
continue;
|
|
29147
|
+
}
|
|
29148
|
+
const key = PERCENTILE_KEYS[r.percentile];
|
|
29149
|
+
const scenario = r.scenario !== void 0 ? findScenario2(u, r.scenario) : void 0;
|
|
29150
|
+
if (r.scenario !== void 0 && !scenario) continue;
|
|
29151
|
+
const what = `${key} of "${u.name}"${scenario ? ` scenario "${scenario.name}"` : ""}`;
|
|
29152
|
+
const name = `${what} < ${formatMs(r.maxMs)}`;
|
|
29153
|
+
const steps = scenario ? scenario.steps : u.scenarios.flatMap((s) => s.steps);
|
|
29154
|
+
const saturated = steps.map((s) => nodes.get(s.toServiceId)).reduce((best, n) => n?.saturated && (!best || n.utilization > best.utilization) ? n : best, void 0);
|
|
29155
|
+
if (saturated) {
|
|
29156
|
+
breaches.push({ requirement: name, message: `${what}: ${saturated.id} is saturated (${usageText(saturated)}) ${under} (limit ${formatMs(r.maxMs)})` });
|
|
29157
|
+
continue;
|
|
29158
|
+
}
|
|
29159
|
+
const ms = scenario ? ua.scenarios[u.scenarios.indexOf(scenario)].percentiles[key] : ua.percentiles[key];
|
|
29160
|
+
if (ms >= r.maxMs) breaches.push({ requirement: name, message: `${what} is ${formatMs(ms)} ${under} (limit ${formatMs(r.maxMs)})` });
|
|
29161
|
+
}
|
|
29162
|
+
}
|
|
29163
|
+
return breaches;
|
|
29164
|
+
}
|
|
29165
|
+
|
|
28492
29166
|
// ../frontend/src/sim/analyze.ts
|
|
28493
29167
|
var DEFAULT_TIMEOUT_MS = 1e3;
|
|
28494
29168
|
var HOT = 0.7;
|
|
@@ -28646,6 +29320,7 @@ function usageText(n) {
|
|
|
28646
29320
|
return `bandwidth ${mb(n.bandwidthLoadMBps)} of ${mb(n.bandwidthCapacityMBps)}, ${formatPercent(n.bandwidthUtilization)}`;
|
|
28647
29321
|
}
|
|
28648
29322
|
if (n.writeScaling === "shards" && n.writeLoadRps > 0) {
|
|
29323
|
+
if (n.readLoadRps <= 0) return `writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
28649
29324
|
return `reads ${part(n.readLoadRps, n.readCapacityRps, n.readUtilization)}; writes ${part(n.writeLoadRps, n.writeCapacityRps, n.writeUtilization)}`;
|
|
28650
29325
|
}
|
|
28651
29326
|
return part(n.loadRps, n.capacityRps, n.requestUtilization);
|
|
@@ -28731,6 +29406,7 @@ function analyze(diagram, options = {}) {
|
|
|
28731
29406
|
const load = computeLoad(diagram, traffic);
|
|
28732
29407
|
const egress = computeEgressGb(diagram, traffic, kindOf2);
|
|
28733
29408
|
const bytes = computeBytesPerSecond(diagram, traffic);
|
|
29409
|
+
const systems = diagram.systems?.length ? analyzeSystems(diagram, traffic, options) : void 0;
|
|
28734
29410
|
const hops = /* @__PURE__ */ new Map();
|
|
28735
29411
|
const nodeResults = nodes.map((n) => {
|
|
28736
29412
|
const p = profiles.get(n.id);
|
|
@@ -28769,12 +29445,13 @@ function analyze(diagram, options = {}) {
|
|
|
28769
29445
|
if (n.saturated) warnings.push(`'${n.id}' is saturated: ${usage}. ${writeBound(n) ? "Add shards for writes" : "Add replicas"} or take load off it.`);
|
|
28770
29446
|
else if (n.utilization > HOT) warnings.push(`'${n.id}' runs hot: ${usage}.`);
|
|
28771
29447
|
}
|
|
29448
|
+
for (const s of systems?.systems ?? []) warnings.push(...s.warnings);
|
|
28772
29449
|
const access = accessIn(diagram);
|
|
28773
29450
|
const pathOf2 = (hopsOfPath) => ({
|
|
28774
29451
|
parts: hopsOfPath.map(
|
|
28775
29452
|
(part) => part.map((h) => {
|
|
28776
29453
|
if (h.failed) return { fixedMs: timeoutOf(h.target), tailMs: 0 };
|
|
28777
|
-
const hop = hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
29454
|
+
const hop = systems?.hop(h.target, h.step.toPort) ?? hops.get(h.target) ?? { fixedMs: 0, tailMs: 0 };
|
|
28778
29455
|
return { fixedMs: transferMs(h.step, bandwidthOf) + hop.fixedMs, tailMs: hop.tailMs };
|
|
28779
29456
|
})
|
|
28780
29457
|
)
|
|
@@ -28804,6 +29481,7 @@ function analyze(diagram, options = {}) {
|
|
|
28804
29481
|
const writes = mainWrites(u, shares, (step) => access(step) === "write");
|
|
28805
29482
|
const availabilityOf = (id) => {
|
|
28806
29483
|
const n = nodeById.get(id);
|
|
29484
|
+
if (!n && systems) return systemAvailability(u, id, systems.availability);
|
|
28807
29485
|
return !n ? 1 : writes.has(id) ? n.writeAvailability : n.availability;
|
|
28808
29486
|
};
|
|
28809
29487
|
return { id: u.id, name: u.name, rps, scenarios, percentiles, tailScenario, availability: useCaseAvailability(u, shares, availabilityOf) };
|
|
@@ -28814,9 +29492,15 @@ function analyze(diagram, options = {}) {
|
|
|
28814
29492
|
totalCostUsd: nodeResults.reduce((sum, n) => sum + n.costUsd, 0),
|
|
28815
29493
|
totalEgressUsd: nodeResults.reduce((sum, n) => sum + n.egressUsd, 0),
|
|
28816
29494
|
singlePointsOfFailure: singlePointsOfFailure(diagram),
|
|
28817
|
-
warnings
|
|
29495
|
+
warnings,
|
|
29496
|
+
...systems ? { systems: systems.systems } : {}
|
|
28818
29497
|
};
|
|
28819
29498
|
}
|
|
29499
|
+
function systemAvailability(u, id, availability2) {
|
|
29500
|
+
let result = 1;
|
|
29501
|
+
for (const s of u.scenarios) for (const step of s.steps) if (step.toServiceId === id) result = Math.min(result, availability2(id, step.toPort) ?? 1);
|
|
29502
|
+
return result;
|
|
29503
|
+
}
|
|
28820
29504
|
function mainWrites(useCase, shares, isWrite) {
|
|
28821
29505
|
if (useCase.scenarios.length === 0) return /* @__PURE__ */ new Set();
|
|
28822
29506
|
const path = criticalPath(useCase, useCase.scenarios[mainScenarioIndex(shares)]).flat();
|
|
@@ -28866,6 +29550,7 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
28866
29550
|
const { passed, message, hint } = requirement(run, r);
|
|
28867
29551
|
return { id: `req:${i + 1}`, name: requirementName(r), category: CATEGORY[r.kind], passed, message, hint, loc: r.loc };
|
|
28868
29552
|
});
|
|
29553
|
+
results.push(...systemResults(diagram, analysis));
|
|
28869
29554
|
const names = /* @__PURE__ */ new Map();
|
|
28870
29555
|
for (const test of diagram.tests ?? []) {
|
|
28871
29556
|
const n = (names.get(test.name) ?? 0) + 1;
|
|
@@ -28874,6 +29559,26 @@ function runTests(diagram, analysis = analyze(diagram), options = {}) {
|
|
|
28874
29559
|
}
|
|
28875
29560
|
return results.map(({ hint, ...r }) => r.passed || !hint ? r : { ...r, hint });
|
|
28876
29561
|
}
|
|
29562
|
+
function systemResults(diagram, analysis) {
|
|
29563
|
+
return (analysis.systems ?? []).filter((s) => s.totalInboundRps > 0 && s.contract.source !== "none").map((s) => {
|
|
29564
|
+
const system = diagram.systems?.find((x) => x.id === s.id);
|
|
29565
|
+
const node = diagram.nodes.find((n) => n.id === s.id);
|
|
29566
|
+
const team = node?.ownerTeam ? `the ${node.ownerTeam} team` : `the owners of ${s.id}`;
|
|
29567
|
+
const problem = s.warnings.find((w) => !w.includes(" runs hot "));
|
|
29568
|
+
const sat = s.contract.entries.find((e) => s.inbound.some((i) => i.entry === e.entry && i.rps > 0));
|
|
29569
|
+
const room = sat && Number.isFinite(sat.saturatesAtRps) ? `; it takes at most ${formatRps(sat.saturatesAtRps)}` : "";
|
|
29570
|
+
const bottleneck = s.saturated && s.busiest ? ` (their bottleneck is ${s.busiest})` : "";
|
|
29571
|
+
return {
|
|
29572
|
+
id: `system:${s.id}`,
|
|
29573
|
+
name: `${s.id} takes what it is sent`,
|
|
29574
|
+
category: s.breaches.some((b) => b.requirement.startsWith("availability")) && !s.saturated ? "availability" : "latency",
|
|
29575
|
+
passed: !problem,
|
|
29576
|
+
message: problem ?? `'${s.id}' takes ${formatRps(s.totalInboundRps)}${(s.sharedInboundRps ?? 0) > s.totalInboundRps * (1 + 1e-9) ? ` (of the ${formatRps(s.sharedInboundRps)} it gets)` : ""}${s.contract.source === "stub" ? "" : " on top of its own traffic"} and holds its requirements${room}`,
|
|
29577
|
+
...problem ? { hint: `Send it less, or ask ${team} to add capacity for it${bottleneck}` } : {},
|
|
29578
|
+
...system ? { loc: system.loc } : {}
|
|
29579
|
+
};
|
|
29580
|
+
});
|
|
29581
|
+
}
|
|
28877
29582
|
var CATEGORY = {
|
|
28878
29583
|
latency: "latency",
|
|
28879
29584
|
availability: "availability",
|
|
@@ -29412,7 +30117,7 @@ var KEYWORDS2 = [
|
|
|
29412
30117
|
{ label: "par", detail: "Steps that run in parallel", snippet: "par {\n $0\n}" },
|
|
29413
30118
|
{ label: "alt", detail: "One scenario of a use case", snippet: 'alt "${1:Scenario}" {\n $0\n}' },
|
|
29414
30119
|
{ label: "legend", detail: 'Styles that .tag applies: owned "Owned by us" green', snippet: 'legend {\n ${1:tag} "${2:What it means}" ${3:blue}$0\n}' },
|
|
29415
|
-
// Layout hints (docs/
|
|
30120
|
+
// Layout hints (docs/STYLING.md, "Steering the layout").
|
|
29416
30121
|
{ label: "direction", detail: "Where edges flow: down, right, up or left (top level or in a group)", snippet: "direction ${1|down,right,up,left|}" },
|
|
29417
30122
|
{ label: "layout ordered", detail: "Lay edges out by declaration order (top level)", snippet: "layout ordered" },
|
|
29418
30123
|
// High-level design sections (top level).
|
|
@@ -29443,14 +30148,282 @@ var ASSERTIONS = [
|
|
|
29443
30148
|
];
|
|
29444
30149
|
|
|
29445
30150
|
// src/imports.ts
|
|
30151
|
+
var import_node_fs4 = require("node:fs");
|
|
30152
|
+
var import_node_path5 = require("node:path");
|
|
30153
|
+
|
|
30154
|
+
// ../frontend/src/dsl/imports.ts
|
|
30155
|
+
var ImportError = class extends Error {
|
|
30156
|
+
name = "ImportError";
|
|
30157
|
+
};
|
|
30158
|
+
var MAX_SPEC_LENGTH = 1024;
|
|
30159
|
+
var SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._ +-]*$/;
|
|
30160
|
+
var HOST = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::\d{1,5})?$/;
|
|
30161
|
+
var REPO_SEGMENT = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
|
|
30162
|
+
var REF = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;
|
|
30163
|
+
var SCHEME = /^([A-Za-z][A-Za-z0-9+.-]+):/;
|
|
30164
|
+
function isRemoteSpec(spec) {
|
|
30165
|
+
return SCHEME.test(spec);
|
|
30166
|
+
}
|
|
30167
|
+
function isAbsolutePath(path) {
|
|
30168
|
+
return /^[\\/]/.test(path) || /^[A-Za-z]:/.test(path);
|
|
30169
|
+
}
|
|
30170
|
+
function checkRef(ref, spec) {
|
|
30171
|
+
if (!REF.test(ref) || ref.length > 200 || ref.includes("..") || ref.includes("//") || ref.endsWith("/") || ref.endsWith(".") || ref.endsWith(".lock") || ref.split("/").some((p) => p.startsWith("."))) {
|
|
30172
|
+
throw new ImportError(`'${spec}' has an invalid ref '${ref}'; use a tag, branch or commit made of letters, digits, '.', '_', '-' and '/'`);
|
|
30173
|
+
}
|
|
30174
|
+
}
|
|
30175
|
+
function repoPathSegments(path, spec) {
|
|
30176
|
+
const parts = path.split("/");
|
|
30177
|
+
if (!path || parts.some((p) => !SEGMENT.test(p) || p === "." || p === "..")) {
|
|
30178
|
+
throw new ImportError(`'${spec}' has an invalid path in the repository; use a relative path like dir/file.proschi, without '..'`);
|
|
30179
|
+
}
|
|
30180
|
+
return parts;
|
|
30181
|
+
}
|
|
30182
|
+
function parseRemoteSpec(spec) {
|
|
30183
|
+
const scheme = SCHEME.exec(spec)?.[1].toLowerCase();
|
|
30184
|
+
if (!scheme) return void 0;
|
|
30185
|
+
if (spec.length > MAX_SPEC_LENGTH) throw new ImportError(`Import source is longer than ${MAX_SPEC_LENGTH} characters`);
|
|
30186
|
+
if (/[\s\0\\]/.test(spec)) throw new ImportError(`'${spec}' contains whitespace or a backslash`);
|
|
30187
|
+
if (scheme === "git") {
|
|
30188
|
+
const body = spec.slice(4);
|
|
30189
|
+
const at = body.lastIndexOf("@");
|
|
30190
|
+
const split2 = body.indexOf("//");
|
|
30191
|
+
if (at === -1 || split2 === -1 || split2 > at) {
|
|
30192
|
+
throw new ImportError(`'${spec}' is not a git source; write git:<host>/<org>/<repo>//<path/in/repo>@<ref>`);
|
|
30193
|
+
}
|
|
30194
|
+
const repo = body.slice(0, split2);
|
|
30195
|
+
const path = body.slice(split2 + 2, at);
|
|
30196
|
+
const ref = body.slice(at + 1);
|
|
30197
|
+
const [host, ...names] = repo.split("/");
|
|
30198
|
+
if (!HOST.test(host) || names.length < 2 || names.some((n) => !REPO_SEGMENT.test(n) || n.endsWith("."))) {
|
|
30199
|
+
throw new ImportError(`'${spec}' names an invalid repository '${repo}'; write <host>/<org>/<repo>`);
|
|
30200
|
+
}
|
|
30201
|
+
repoPathSegments(path, spec);
|
|
30202
|
+
checkRef(ref, spec);
|
|
30203
|
+
return { kind: "git", repo, path, ref };
|
|
30204
|
+
}
|
|
30205
|
+
if (scheme === "http") throw new ImportError(`'${spec}' uses http://; only https:// imports are allowed`);
|
|
30206
|
+
if (scheme !== "https") throw new ImportError(`'${spec}' is not a supported import source; use a relative path, git: or https://`);
|
|
30207
|
+
let url;
|
|
30208
|
+
try {
|
|
30209
|
+
url = new URL(spec);
|
|
30210
|
+
} catch {
|
|
30211
|
+
throw new ImportError(`'${spec}' is not a valid URL`);
|
|
30212
|
+
}
|
|
30213
|
+
if (url.username || url.password) throw new ImportError(`'${spec}' contains credentials; git and https imports use your own credentials, never ones in the diagram`);
|
|
30214
|
+
if (url.hash) throw new ImportError(`'${spec}' has a #fragment; remove it`);
|
|
30215
|
+
if (!HOST.test(url.host)) throw new ImportError(`'${spec}' has an invalid host`);
|
|
30216
|
+
return { kind: "https", url: url.href };
|
|
30217
|
+
}
|
|
30218
|
+
function formatRemoteSpec(spec) {
|
|
30219
|
+
return spec.kind === "git" ? `git:${spec.repo}//${spec.path}@${spec.ref}` : spec.url;
|
|
30220
|
+
}
|
|
30221
|
+
function resolveImportPath(fromPath, importPath) {
|
|
30222
|
+
if (importPath.includes("\0")) throw new ImportError(`'${importPath.replace(/\0/g, "")}' contains a NUL character`);
|
|
30223
|
+
const remote = parseRemoteSpec(importPath);
|
|
30224
|
+
if (remote) return formatRemoteSpec(remote);
|
|
30225
|
+
if (isAbsolutePath(importPath)) throw new ImportError(`'${importPath}' is an absolute path; imports must be relative to the importing file`);
|
|
30226
|
+
const from = fromPath && isRemoteSpec(fromPath) ? parseRemoteSpec(fromPath) : void 0;
|
|
30227
|
+
if (from?.kind === "git") {
|
|
30228
|
+
const parts2 = from.path.split("/").slice(0, -1);
|
|
30229
|
+
for (const part of importPath.split("/")) {
|
|
30230
|
+
if (part === "" || part === ".") continue;
|
|
30231
|
+
if (part !== "..") parts2.push(part);
|
|
30232
|
+
else if (parts2.length > 0) parts2.pop();
|
|
30233
|
+
else throw new ImportError(`'${importPath}' leaves the repository ${from.repo}`);
|
|
30234
|
+
}
|
|
30235
|
+
repoPathSegments(parts2.join("/"), importPath);
|
|
30236
|
+
return formatRemoteSpec({ ...from, path: parts2.join("/") });
|
|
30237
|
+
}
|
|
30238
|
+
if (from?.kind === "https") {
|
|
30239
|
+
const base = new URL(from.url);
|
|
30240
|
+
const url = new URL(importPath, base);
|
|
30241
|
+
if (url.origin !== base.origin) throw new ImportError(`'${importPath}' leaves ${base.origin}`);
|
|
30242
|
+
return formatRemoteSpec(parseRemoteSpec(url.href));
|
|
30243
|
+
}
|
|
30244
|
+
const parts = !fromPath ? [] : fromPath.split("/").slice(0, -1);
|
|
30245
|
+
for (const part of importPath.split("/")) {
|
|
30246
|
+
if (part === "" || part === ".") continue;
|
|
30247
|
+
if (part === ".." && parts.length > 0 && parts[parts.length - 1] !== "..") parts.pop();
|
|
30248
|
+
else parts.push(part);
|
|
30249
|
+
}
|
|
30250
|
+
return parts.join("/");
|
|
30251
|
+
}
|
|
30252
|
+
|
|
30253
|
+
// src/lock.ts
|
|
30254
|
+
var import_node_crypto = require("node:crypto");
|
|
29446
30255
|
var import_node_fs3 = require("node:fs");
|
|
29447
30256
|
var import_node_path4 = require("node:path");
|
|
29448
|
-
|
|
29449
|
-
|
|
29450
|
-
|
|
29451
|
-
|
|
29452
|
-
|
|
30257
|
+
|
|
30258
|
+
// src/lockFormat.ts
|
|
30259
|
+
var LOCK_FILE = "proschi.lock";
|
|
30260
|
+
var CACHE_DIR = ".proschi/cache";
|
|
30261
|
+
var HASH = /^[0-9a-f]{64}$/;
|
|
30262
|
+
var COMMIT = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
30263
|
+
var emptyLock = () => ({ version: 1, git: {}, https: {} });
|
|
30264
|
+
var gitKey = (repo, ref) => `${repo}@${ref}`;
|
|
30265
|
+
var remoteError = {
|
|
30266
|
+
noLock: (spec) => new ImportError(`'${spec}' is a remote import and there is no ${LOCK_FILE}; run proschi imports sync`),
|
|
30267
|
+
notLocked: (spec) => new ImportError(`'${spec}' is not in ${LOCK_FILE}; run proschi imports sync`),
|
|
30268
|
+
notCached: (spec) => new ImportError(`'${spec}' is not in the import cache (${CACHE_DIR}); run proschi imports sync`),
|
|
30269
|
+
altered: (spec) => new ImportError(`The cached copy of '${spec}' does not match its hash in ${LOCK_FILE}; run proschi imports sync`)
|
|
30270
|
+
};
|
|
30271
|
+
var isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
30272
|
+
function parseLock(text) {
|
|
30273
|
+
const invalid = (why) => new ImportError(`Invalid ${LOCK_FILE}: ${why}; delete it and run proschi imports sync`);
|
|
30274
|
+
let json;
|
|
30275
|
+
try {
|
|
30276
|
+
json = JSON.parse(text);
|
|
30277
|
+
} catch (e) {
|
|
30278
|
+
throw invalid(e.message);
|
|
30279
|
+
}
|
|
30280
|
+
if (!isRecord(json) || json.version !== 1) throw invalid('expected {"version": 1, \u2026}');
|
|
30281
|
+
const lock = emptyLock();
|
|
30282
|
+
const git = json.git ?? {};
|
|
30283
|
+
const https = json.https ?? {};
|
|
30284
|
+
if (!isRecord(git) || !isRecord(https)) throw invalid('"git" and "https" must be objects');
|
|
30285
|
+
for (const [key, entry] of Object.entries(git)) {
|
|
30286
|
+
const at = key.lastIndexOf("@");
|
|
30287
|
+
const valid = at > 0 && isRecord(entry) && typeof entry.commit === "string" && COMMIT.test(entry.commit) && isRecord(entry.files) && Object.entries(entry.files).every(([path, hash]) => typeof hash === "string" && HASH.test(hash) && isGitSpec(`git:${key.slice(0, at)}//${path}@${key.slice(at + 1)}`));
|
|
30288
|
+
if (!valid) throw invalid(`bad git entry '${key}'`);
|
|
30289
|
+
lock.git[key] = { commit: entry.commit, files: { ...entry.files } };
|
|
30290
|
+
}
|
|
30291
|
+
for (const [url, hash] of Object.entries(https)) {
|
|
30292
|
+
if (typeof hash !== "string" || !HASH.test(hash) || parseRemoteSpec(url)?.kind !== "https") throw invalid(`bad https entry '${url}'`);
|
|
30293
|
+
lock.https[url] = hash;
|
|
30294
|
+
}
|
|
30295
|
+
return lock;
|
|
30296
|
+
}
|
|
30297
|
+
function isGitSpec(spec) {
|
|
30298
|
+
try {
|
|
30299
|
+
return parseRemoteSpec(spec)?.kind === "git";
|
|
30300
|
+
} catch {
|
|
30301
|
+
return false;
|
|
30302
|
+
}
|
|
30303
|
+
}
|
|
30304
|
+
function lockedHash(lock, spec) {
|
|
30305
|
+
const remote = parseRemoteSpec(spec);
|
|
30306
|
+
if (remote?.kind === "git") return lock.git[gitKey(remote.repo, remote.ref)]?.files[remote.path];
|
|
30307
|
+
if (remote?.kind === "https") return lock.https[remote.url];
|
|
30308
|
+
return void 0;
|
|
30309
|
+
}
|
|
30310
|
+
|
|
30311
|
+
// src/lock.ts
|
|
30312
|
+
var sha256 = (bytes) => (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
30313
|
+
function readLock(root) {
|
|
30314
|
+
const file = (0, import_node_path4.join)(root, LOCK_FILE);
|
|
30315
|
+
return (0, import_node_fs3.existsSync)(file) ? parseLock((0, import_node_fs3.readFileSync)(file, "utf8")) : void 0;
|
|
30316
|
+
}
|
|
30317
|
+
var cacheFile = (root, hash) => (0, import_node_path4.join)(root, CACHE_DIR, hash);
|
|
30318
|
+
function readCached(root, hash, spec) {
|
|
30319
|
+
if (!HASH.test(hash)) return void 0;
|
|
30320
|
+
const file = cacheFile(root, hash);
|
|
30321
|
+
if (!(0, import_node_fs3.existsSync)(file)) return void 0;
|
|
30322
|
+
const bytes = (0, import_node_fs3.readFileSync)(file);
|
|
30323
|
+
if (sha256(bytes) !== hash) throw remoteError.altered(spec);
|
|
30324
|
+
return bytes;
|
|
30325
|
+
}
|
|
30326
|
+
|
|
30327
|
+
// src/imports.ts
|
|
30328
|
+
function displayPath2(file, cwd = process.cwd()) {
|
|
30329
|
+
const abs = (0, import_node_path5.resolve)(cwd, file);
|
|
30330
|
+
const rel = (0, import_node_path5.relative)(cwd, abs);
|
|
30331
|
+
return rel && !rel.startsWith("..") && !(0, import_node_path5.isAbsolute)(rel) ? rel : abs;
|
|
30332
|
+
}
|
|
30333
|
+
var MAX_IMPORT_BYTES = 1e6;
|
|
30334
|
+
var MAX_IMPORT_FILES = 500;
|
|
30335
|
+
var MAX_IMPORT_DEPTH = 32;
|
|
30336
|
+
function projectRoot(entry) {
|
|
30337
|
+
const dir = (0, import_node_path5.dirname)((0, import_node_path5.resolve)(entry));
|
|
30338
|
+
const config = findConfigFile(dir);
|
|
30339
|
+
if (config) return (0, import_node_fs4.realpathSync)((0, import_node_path5.dirname)(config));
|
|
30340
|
+
for (let current = dir; ; current = (0, import_node_path5.dirname)(current)) {
|
|
30341
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(current, ".git"))) return (0, import_node_fs4.realpathSync)(current);
|
|
30342
|
+
if ((0, import_node_path5.dirname)(current) === current) break;
|
|
30343
|
+
}
|
|
30344
|
+
return (0, import_node_fs4.realpathSync)((0, import_node_fs4.existsSync)(dir) ? dir : process.cwd());
|
|
30345
|
+
}
|
|
30346
|
+
var isInside = (root, path) => {
|
|
30347
|
+
const rel = (0, import_node_path5.relative)(root, path);
|
|
30348
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${import_node_path5.sep}`) && !(0, import_node_path5.isAbsolute)(rel);
|
|
30349
|
+
};
|
|
30350
|
+
function realLocation(path) {
|
|
30351
|
+
let existing = path;
|
|
30352
|
+
while (!(0, import_node_fs4.existsSync)(existing) && (0, import_node_path5.dirname)(existing) !== existing) existing = (0, import_node_path5.dirname)(existing);
|
|
30353
|
+
return (0, import_node_path5.join)((0, import_node_fs4.realpathSync)(existing), (0, import_node_path5.relative)(existing, path));
|
|
30354
|
+
}
|
|
30355
|
+
var tooLarge = (what) => new ImportError(`'${what}' is larger than ${MAX_IMPORT_BYTES / 1e6} MB`);
|
|
30356
|
+
function fileResolver({ entry, read = () => void 0, toPath = (p) => p, remote } = {}) {
|
|
30357
|
+
const root = projectRoot(entry ?? (0, import_node_path5.join)(process.cwd(), "stdin.proschi"));
|
|
30358
|
+
const errors = /* @__PURE__ */ new Map();
|
|
30359
|
+
const levels = /* @__PURE__ */ new Map();
|
|
30360
|
+
let lock = null;
|
|
30361
|
+
const fromCache = (spec) => {
|
|
30362
|
+
if (lock === null) lock = readLock(root);
|
|
30363
|
+
if (!lock) throw remoteError.noLock(spec);
|
|
30364
|
+
const hash = lockedHash(lock, spec);
|
|
30365
|
+
if (!hash) throw remoteError.notLocked(spec);
|
|
30366
|
+
const bytes = readCached(root, hash, spec);
|
|
30367
|
+
if (!bytes) throw remoteError.notCached(spec);
|
|
30368
|
+
return bytes.toString("utf8");
|
|
30369
|
+
};
|
|
30370
|
+
const loadRemote = remote ?? fromCache;
|
|
30371
|
+
const found = (path, source2, level) => {
|
|
30372
|
+
if (!levels.has(path)) {
|
|
30373
|
+
if (levels.size >= MAX_IMPORT_FILES) throw new ImportError(`More than ${MAX_IMPORT_FILES} imported files; split the diagram`);
|
|
30374
|
+
levels.set(path, level);
|
|
30375
|
+
}
|
|
30376
|
+
return { path, source: source2 };
|
|
30377
|
+
};
|
|
30378
|
+
const resolveOne = (importPath, fromPath) => {
|
|
30379
|
+
const level = (fromPath === void 0 ? 0 : levels.get(fromPath) ?? 0) + 1;
|
|
30380
|
+
if (level > MAX_IMPORT_DEPTH) throw new ImportError(`Imports nest more than ${MAX_IMPORT_DEPTH} files deep`);
|
|
30381
|
+
if (isRemoteSpec(importPath) || fromPath !== void 0 && isRemoteSpec(fromPath)) {
|
|
30382
|
+
const spec = resolveImportPath(fromPath, importPath);
|
|
30383
|
+
const source2 = loadRemote(spec);
|
|
30384
|
+
if (source2 === void 0) return void 0;
|
|
30385
|
+
if (Buffer.byteLength(source2) > MAX_IMPORT_BYTES) throw tooLarge(spec);
|
|
30386
|
+
return found(spec, source2, level);
|
|
30387
|
+
}
|
|
30388
|
+
resolveImportPath(void 0, importPath);
|
|
30389
|
+
const abs = (0, import_node_path5.resolve)(fromPath ? (0, import_node_path5.dirname)((0, import_node_path5.resolve)(fromPath)) : root, importPath);
|
|
30390
|
+
if (!isInside(root, realLocation(abs))) {
|
|
30391
|
+
const link = (0, import_node_fs4.existsSync)(abs) && (0, import_node_fs4.lstatSync)(abs).isSymbolicLink();
|
|
30392
|
+
throw new ImportError(
|
|
30393
|
+
link ? `'${importPath}' is a symlink to a file outside the project root ${displayPath2(root)}` : `'${importPath}' is outside the project root ${displayPath2(root)}; imports must stay inside the folder of proschi.json (else the git root)`
|
|
30394
|
+
);
|
|
30395
|
+
}
|
|
30396
|
+
const text = read(abs);
|
|
30397
|
+
if (text !== void 0) {
|
|
30398
|
+
if (Buffer.byteLength(text) > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
30399
|
+
return found(toPath(abs), text, level);
|
|
30400
|
+
}
|
|
30401
|
+
if (!(0, import_node_fs4.existsSync)(abs)) return void 0;
|
|
30402
|
+
const stat = (0, import_node_fs4.statSync)(abs);
|
|
30403
|
+
if (!stat.isFile()) return void 0;
|
|
30404
|
+
if (stat.size > MAX_IMPORT_BYTES) throw tooLarge(importPath);
|
|
30405
|
+
return found(toPath(abs), (0, import_node_fs4.readFileSync)((0, import_node_fs4.realpathSync)(abs), "utf8"), level);
|
|
30406
|
+
};
|
|
30407
|
+
const resolver = ((importPath, fromPath) => {
|
|
30408
|
+
try {
|
|
30409
|
+
return resolveOne(importPath, fromPath);
|
|
30410
|
+
} catch (e) {
|
|
30411
|
+
if (e instanceof ImportError) errors.set(`${fromPath ?? ""}\0${importPath}`, e.message);
|
|
30412
|
+
throw e;
|
|
30413
|
+
}
|
|
30414
|
+
});
|
|
30415
|
+
resolver.root = root;
|
|
30416
|
+
resolver.errors = errors;
|
|
30417
|
+
resolver.explain = (result) => {
|
|
30418
|
+
for (const i of result.imports ?? []) {
|
|
30419
|
+
if (i.resolved !== void 0) continue;
|
|
30420
|
+
const reason = errors.get(`${i.loc.file ?? entry ?? ""}\0${i.path}`);
|
|
30421
|
+
const d = reason && result.diagnostics.find((d2) => d2.file === i.loc.file && d2.line === i.loc.line && d2.col === i.loc.col && d2.message === `Cannot find '${i.path}'`);
|
|
30422
|
+
if (d) d.message = reason;
|
|
30423
|
+
}
|
|
30424
|
+
return result;
|
|
29453
30425
|
};
|
|
30426
|
+
return resolver;
|
|
29454
30427
|
}
|
|
29455
30428
|
function reachableFrom(result, start) {
|
|
29456
30429
|
const reached = /* @__PURE__ */ new Set([start]);
|
|
@@ -29472,12 +30445,12 @@ function ownDiagnostics(result) {
|
|
|
29472
30445
|
if (i.loc.file !== void 0 || i.resolved === void 0) continue;
|
|
29473
30446
|
const files = reachableFrom(result, i.resolved);
|
|
29474
30447
|
const errors = result.diagnostics.filter((d) => d.severity === "error" && d.file !== void 0 && files.has(d.file)).length;
|
|
29475
|
-
if (errors) own.push({ severity: "error", message: `'${(0,
|
|
30448
|
+
if (errors) own.push({ severity: "error", message: `'${(0, import_node_path5.basename)(i.path)}' has ${errors} error${errors === 1 ? "" : "s"}`, ...i.loc });
|
|
29476
30449
|
}
|
|
29477
30450
|
return own.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
29478
30451
|
}
|
|
29479
30452
|
function importLinks(result) {
|
|
29480
|
-
return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0).map((i) => {
|
|
30453
|
+
return (result.imports ?? []).filter((i) => i.loc.file === void 0 && i.resolved !== void 0 && !isRemoteSpec(i.resolved)).map((i) => {
|
|
29481
30454
|
const range = toRange(i.loc);
|
|
29482
30455
|
return {
|
|
29483
30456
|
range: { start: { ...range.start, character: range.start.character + 1 }, end: { ...range.end, character: Math.max(range.start.character + 1, range.end.character - 1) } },
|
|
@@ -29784,7 +30757,7 @@ function positionAt(text, offset) {
|
|
|
29784
30757
|
|
|
29785
30758
|
// src/server.ts
|
|
29786
30759
|
if (process.argv.includes("--version")) {
|
|
29787
|
-
console.log(true ? "0.
|
|
30760
|
+
console.log(true ? "0.13.0" : "dev");
|
|
29788
30761
|
process.exit(0);
|
|
29789
30762
|
}
|
|
29790
30763
|
var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
|
|
@@ -29795,11 +30768,13 @@ var uriOf = (path) => (0, import_node_url.pathToFileURL)(path).href;
|
|
|
29795
30768
|
function openText(path) {
|
|
29796
30769
|
return documents.all().find((d) => pathOf(d.uri) === path)?.getText();
|
|
29797
30770
|
}
|
|
29798
|
-
var resolve3 = fileResolver(openText);
|
|
29799
30771
|
function analysisOf(doc) {
|
|
29800
30772
|
const cached = cache2.get(doc.uri);
|
|
29801
30773
|
if (cached?.version === doc.version) return cached.analysis;
|
|
29802
|
-
const
|
|
30774
|
+
const path = pathOf(doc.uri);
|
|
30775
|
+
const resolve3 = fileResolver({ entry: path, read: openText });
|
|
30776
|
+
const analysis = analyze2(doc.getText(), { path, resolve: resolve3 });
|
|
30777
|
+
resolve3.explain(analysis.result);
|
|
29803
30778
|
const files = new Set((analysis.result.imports ?? []).flatMap((i) => i.resolved ? [i.resolved] : []));
|
|
29804
30779
|
cache2.set(doc.uri, { version: doc.version, analysis, files });
|
|
29805
30780
|
return analysis;
|
|
@@ -29828,7 +30803,7 @@ connection.onInitialize(() => ({
|
|
|
29828
30803
|
codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] },
|
|
29829
30804
|
semanticTokensProvider: { legend: { tokenTypes: TOKEN_TYPES, tokenModifiers: [] }, full: true }
|
|
29830
30805
|
},
|
|
29831
|
-
serverInfo: { name: "proschi-language-server", version: true ? "0.
|
|
30806
|
+
serverInfo: { name: "proschi-language-server", version: true ? "0.13.0" : "dev" }
|
|
29832
30807
|
}));
|
|
29833
30808
|
function validate(document) {
|
|
29834
30809
|
const analysis = analysisOf(document);
|
|
@@ -29872,7 +30847,7 @@ documents.onDidChangeContent(({ document }) => {
|
|
|
29872
30847
|
function watchedStamp() {
|
|
29873
30848
|
return watchedFiles().map((file) => {
|
|
29874
30849
|
try {
|
|
29875
|
-
return `${file}:${(0,
|
|
30850
|
+
return `${file}:${(0, import_node_fs5.statSync)(file).mtimeMs}`;
|
|
29876
30851
|
} catch {
|
|
29877
30852
|
return `${file}:missing`;
|
|
29878
30853
|
}
|