@emseepea/server 0.23.0 → 0.24.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/app-shutdown.d.ts +11 -0
- package/dist/app-shutdown.d.ts.map +1 -0
- package/dist/app-shutdown.js +46 -0
- package/dist/app-shutdown.js.map +1 -0
- package/dist/events-external.d.ts +62 -0
- package/dist/events-external.d.ts.map +1 -0
- package/dist/events-external.js +289 -0
- package/dist/events-external.js.map +1 -0
- package/dist/events.d.ts +30 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +146 -134
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -88
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -8,9 +8,13 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { readdir, realpath } from "node:fs/promises";
|
|
9
9
|
import { isIP } from "node:net";
|
|
10
10
|
import { isAbsolute, join } from "node:path";
|
|
11
|
+
import { performance } from "node:perf_hooks";
|
|
11
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
13
|
import { z } from "zod";
|
|
14
|
+
import { prepareAppShutdown, remainingAppShutdown, startAppShutdown, registerAppShutdownOwner } from "./app-shutdown.js";
|
|
13
15
|
import { createMcpEventRuntime, McpEventInputError } from "./events.js";
|
|
16
|
+
import { createExternalMcpEventRuntime } from "./events-external.js";
|
|
17
|
+
export { ExternalMcpEventError } from "./events-external.js";
|
|
14
18
|
import { createResourceInventory, ResourceInventoryCursorError } from "./resource-inventory.js";
|
|
15
19
|
import { installObservability, observabilityState, } from "./telemetry.js";
|
|
16
20
|
export { openTelemetry, structuredLogging, } from "./telemetry.js";
|
|
@@ -1093,6 +1097,8 @@ export function createEmseepea(options) {
|
|
|
1093
1097
|
const readinessTimeoutMs = callbackTimeout("readiness", readiness, "readinessTimeoutMs", options.readinessTimeoutMs);
|
|
1094
1098
|
const stopping = new AbortController();
|
|
1095
1099
|
let eventRuntime;
|
|
1100
|
+
let internalEventRuntime;
|
|
1101
|
+
let externalEventRuntime;
|
|
1096
1102
|
let pendingReadiness;
|
|
1097
1103
|
async function ready() {
|
|
1098
1104
|
if (stopping.signal.aborted)
|
|
@@ -1173,7 +1179,14 @@ export function createEmseepea(options) {
|
|
|
1173
1179
|
if (options.events && !authentication) {
|
|
1174
1180
|
throw new TypeError("MCP Events require authentication configuration");
|
|
1175
1181
|
}
|
|
1176
|
-
|
|
1182
|
+
if (options.events) {
|
|
1183
|
+
if ("mode" in options.events) {
|
|
1184
|
+
eventRuntime = externalEventRuntime = createExternalMcpEventRuntime(options.events);
|
|
1185
|
+
}
|
|
1186
|
+
else {
|
|
1187
|
+
eventRuntime = internalEventRuntime = createMcpEventRuntime(options.events);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1177
1190
|
const hasStreaming = tools.some((tool) => tool[TOOL_STREAMING]);
|
|
1178
1191
|
const hasProgress = hasStreaming || resources.length > 0 || prompts.length > 0;
|
|
1179
1192
|
if ([
|
|
@@ -1332,6 +1345,22 @@ export function createEmseepea(options) {
|
|
|
1332
1345
|
});
|
|
1333
1346
|
const nodeHandler = toNodeHandler(sdkHandler);
|
|
1334
1347
|
const app = createMcpFastifyApp({ host: deployment.mode === "loopback" ? "127.0.0.1" : "0.0.0.0" });
|
|
1348
|
+
const unstartedSockets = new Set();
|
|
1349
|
+
app.server.on("connection", (socket) => {
|
|
1350
|
+
if (stopping.signal.aborted) {
|
|
1351
|
+
socket.destroy();
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
unstartedSockets.add(socket);
|
|
1355
|
+
socket.once("close", () => unstartedSockets.delete(socket));
|
|
1356
|
+
});
|
|
1357
|
+
app.server.on("request", (request, response) => {
|
|
1358
|
+
unstartedSockets.delete(request.socket);
|
|
1359
|
+
response.once("finish", () => {
|
|
1360
|
+
if (stopping.signal.aborted)
|
|
1361
|
+
queueMicrotask(() => app.server.closeIdleConnections());
|
|
1362
|
+
});
|
|
1363
|
+
});
|
|
1335
1364
|
async function serveResourceSubscription(request, reply, uri) {
|
|
1336
1365
|
if (!resourceSubscriptions || !resourceEvents)
|
|
1337
1366
|
return;
|
|
@@ -1421,7 +1450,11 @@ export function createEmseepea(options) {
|
|
|
1421
1450
|
.code(available ? 200 : 503).send(available ? "ready\n" : "not ready\n");
|
|
1422
1451
|
});
|
|
1423
1452
|
app.addHook("preClose", async () => {
|
|
1453
|
+
prepareAppShutdown(app);
|
|
1454
|
+
void startAppShutdown(app);
|
|
1424
1455
|
stopping.abort();
|
|
1456
|
+
for (const socket of unstartedSockets)
|
|
1457
|
+
socket.destroy();
|
|
1425
1458
|
await Promise.all([...activeSubscriptions].map((handler) => handler.close()));
|
|
1426
1459
|
});
|
|
1427
1460
|
if (authentication) {
|
|
@@ -1442,6 +1475,7 @@ export function createEmseepea(options) {
|
|
|
1442
1475
|
handler: async (_request, reply) => { await reply.header("allow", "POST").code(405).send(); },
|
|
1443
1476
|
});
|
|
1444
1477
|
app.post("/mcp", { bodyLimit: maxRequestBytes }, async (request, reply) => {
|
|
1478
|
+
const admittedAt = performance.now();
|
|
1445
1479
|
if (deployment.mode === "production-behind-proxy" &&
|
|
1446
1480
|
!validateProductionRequest(request, reply, deployment, limiter))
|
|
1447
1481
|
return;
|
|
@@ -1538,95 +1572,111 @@ export function createEmseepea(options) {
|
|
|
1538
1572
|
}
|
|
1539
1573
|
if (eventRuntime && !legacy && isRecord(request.body) &&
|
|
1540
1574
|
(request.body.method === "server/discover" || eventMethod)) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
if (request
|
|
1546
|
-
|
|
1547
|
-
? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], principal)) : tools;
|
|
1548
|
-
const activeResources = protectsCatalogue
|
|
1549
|
-
? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], principal)) : resources;
|
|
1550
|
-
const activePrompts = protectsCatalogue
|
|
1551
|
-
? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], principal)) : prompts;
|
|
1552
|
-
const capabilities = {
|
|
1553
|
-
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
1554
|
-
...(activeResources.length ? { resources: {
|
|
1555
|
-
subscribe: Boolean(resourceSubscriptions), listChanged: false,
|
|
1556
|
-
} } : {}),
|
|
1557
|
-
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
1558
|
-
...(activeResources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
1559
|
-
activePrompts.some((prompt) => prompt[HAS_COMPLETION]) ? { completions: {} } : {}),
|
|
1560
|
-
...(clientLogging ? { logging: {} } : {}),
|
|
1561
|
-
events: {},
|
|
1562
|
-
};
|
|
1563
|
-
await reply.code(200).send({
|
|
1564
|
-
jsonrpc: "2.0",
|
|
1565
|
-
id: requestId(request.body.id),
|
|
1566
|
-
result: {
|
|
1567
|
-
supportedVersions: [PROTOCOL_VERSION],
|
|
1568
|
-
capabilities,
|
|
1569
|
-
resultType: "complete",
|
|
1570
|
-
ttlMs: protectsCatalogue ? 0 : cacheHints?.["server/discover"]?.ttlMs ?? 0,
|
|
1571
|
-
cacheScope: protectsCatalogue ? "private" : cacheHints?.["server/discover"]?.cacheScope ?? "private",
|
|
1572
|
-
...(options.instructions ? { instructions: options.instructions } : {}),
|
|
1573
|
-
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
1574
|
-
},
|
|
1575
|
-
});
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
if (!authInfo)
|
|
1579
|
-
throw new Error("Events reached execution without verified authorization");
|
|
1580
|
-
let ownerKey;
|
|
1575
|
+
const disconnectedEvents = new AbortController();
|
|
1576
|
+
const abortEvents = () => disconnectedEvents.abort();
|
|
1577
|
+
request.raw.once("aborted", abortEvents);
|
|
1578
|
+
reply.raw.once("close", abortEvents);
|
|
1579
|
+
if (requestAlreadyClosed(request, reply))
|
|
1580
|
+
abortEvents();
|
|
1581
1581
|
try {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1582
|
+
const requestEvents = externalEventRuntime ? externalEventRuntime.forInvocation({
|
|
1583
|
+
signal: AbortSignal.any([stopping.signal, disconnectedEvents.signal]),
|
|
1584
|
+
deadlineMs: Date.now() + Math.max(0, operationTimeoutMs - (performance.now() - admittedAt)),
|
|
1585
|
+
}) : eventRuntime;
|
|
1586
|
+
if (!await requestEvents.ready()) {
|
|
1587
|
+
await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (request.body.method === "server/discover") {
|
|
1591
|
+
const activeTools = protectsCatalogue
|
|
1592
|
+
? tools.filter((tool) => accessAllows(tool[TOOL_ACCESS], principal)) : tools;
|
|
1593
|
+
const activeResources = protectsCatalogue
|
|
1594
|
+
? resources.filter((resource) => accessAllows(resource[RESOURCE_ACCESS], principal)) : resources;
|
|
1595
|
+
const activePrompts = protectsCatalogue
|
|
1596
|
+
? prompts.filter((prompt) => accessAllows(prompt[PROMPT_ACCESS], principal)) : prompts;
|
|
1597
|
+
const capabilities = {
|
|
1598
|
+
...(activeTools.length ? { tools: { listChanged: false } } : {}),
|
|
1599
|
+
...(activeResources.length ? { resources: {
|
|
1600
|
+
subscribe: Boolean(resourceSubscriptions), listChanged: false,
|
|
1601
|
+
} } : {}),
|
|
1602
|
+
...(activePrompts.length ? { prompts: { listChanged: false } } : {}),
|
|
1603
|
+
...(activeResources.some((resource) => resource[HAS_COMPLETION]) ||
|
|
1604
|
+
activePrompts.some((prompt) => prompt[HAS_COMPLETION]) ? { completions: {} } : {}),
|
|
1605
|
+
...(clientLogging ? { logging: {} } : {}),
|
|
1606
|
+
events: {},
|
|
1607
|
+
};
|
|
1591
1608
|
await reply.code(200).send({
|
|
1592
1609
|
jsonrpc: "2.0",
|
|
1593
1610
|
id: requestId(request.body.id),
|
|
1594
|
-
result: {
|
|
1611
|
+
result: {
|
|
1612
|
+
supportedVersions: [PROTOCOL_VERSION],
|
|
1613
|
+
capabilities,
|
|
1614
|
+
resultType: "complete",
|
|
1615
|
+
ttlMs: protectsCatalogue ? 0 : cacheHints?.["server/discover"]?.ttlMs ?? 0,
|
|
1616
|
+
cacheScope: protectsCatalogue ? "private" : cacheHints?.["server/discover"]?.cacheScope ?? "private",
|
|
1617
|
+
...(options.instructions ? { instructions: options.instructions } : {}),
|
|
1618
|
+
_meta: { "io.modelcontextprotocol/serverInfo": serverInfo },
|
|
1619
|
+
},
|
|
1595
1620
|
});
|
|
1621
|
+
return;
|
|
1596
1622
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
if (request.body.method === "events/subscribe") {
|
|
1623
|
+
if (!authInfo)
|
|
1624
|
+
throw new Error("Events reached execution without verified authorization");
|
|
1625
|
+
let ownerKey;
|
|
1603
1626
|
try {
|
|
1604
|
-
|
|
1605
|
-
await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result });
|
|
1627
|
+
ownerKey = await requestEvents.ownerKey(authInfo);
|
|
1606
1628
|
}
|
|
1607
|
-
catch
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
|
|
1629
|
+
catch {
|
|
1630
|
+
await sendRpcError(reply, 500, -32603, "Event owner identity unavailable", requestId(request.body.id));
|
|
1631
|
+
return;
|
|
1613
1632
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1633
|
+
if (request.body.method === "events/list") {
|
|
1634
|
+
try {
|
|
1635
|
+
const events = await requestEvents.list(ownerKey);
|
|
1636
|
+
await reply.code(200).send({
|
|
1637
|
+
jsonrpc: "2.0",
|
|
1638
|
+
id: requestId(request.body.id),
|
|
1639
|
+
result: { events, _meta: { "io.modelcontextprotocol/serverInfo": serverInfo } },
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
|
|
1644
|
+
}
|
|
1645
|
+
return;
|
|
1620
1646
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1647
|
+
if (request.body.method === "events/subscribe") {
|
|
1648
|
+
try {
|
|
1649
|
+
const result = await requestEvents.subscribe(ownerKey, request.body.params);
|
|
1650
|
+
await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result });
|
|
1624
1651
|
}
|
|
1625
|
-
|
|
1652
|
+
catch (error) {
|
|
1653
|
+
if (error instanceof McpEventInputError) {
|
|
1654
|
+
await sendRpcError(reply, error.code === -32015 ? 200 : 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1626
1657
|
await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
|
|
1627
1658
|
}
|
|
1659
|
+
return;
|
|
1628
1660
|
}
|
|
1629
|
-
|
|
1661
|
+
if (request.body.method === "events/unsubscribe") {
|
|
1662
|
+
try {
|
|
1663
|
+
await requestEvents.unsubscribe(ownerKey, request.body.params);
|
|
1664
|
+
await reply.code(200).send({ jsonrpc: "2.0", id: requestId(request.body.id), result: {} });
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
if (error instanceof McpEventInputError) {
|
|
1668
|
+
await sendRpcError(reply, 400, error.code, error.message, requestId(request.body.id), { reason: error.reason });
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
await sendRpcError(reply, 503, -32603, "Events are unavailable", requestId(request.body.id));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
finally {
|
|
1678
|
+
request.raw.off("aborted", abortEvents);
|
|
1679
|
+
reply.raw.off("close", abortEvents);
|
|
1630
1680
|
}
|
|
1631
1681
|
}
|
|
1632
1682
|
if (subscriptionTarget) {
|
|
@@ -1644,7 +1694,7 @@ export function createEmseepea(options) {
|
|
|
1644
1694
|
try {
|
|
1645
1695
|
await requestOperations.run({
|
|
1646
1696
|
deadlineMs: Date.now() + operationTimeoutMs,
|
|
1647
|
-
signal: disconnected.signal,
|
|
1697
|
+
signal: AbortSignal.any([disconnected.signal, stopping.signal]),
|
|
1648
1698
|
principal,
|
|
1649
1699
|
filterCatalogues: protectsCatalogue,
|
|
1650
1700
|
capability,
|
|
@@ -1670,17 +1720,32 @@ export function createEmseepea(options) {
|
|
|
1670
1720
|
}
|
|
1671
1721
|
});
|
|
1672
1722
|
let drainingEvents = false;
|
|
1673
|
-
const eventTimer =
|
|
1723
|
+
const eventTimer = internalEventRuntime ? setInterval(() => {
|
|
1674
1724
|
if (drainingEvents || stopping.signal.aborted)
|
|
1675
1725
|
return;
|
|
1676
1726
|
drainingEvents = true;
|
|
1677
|
-
void
|
|
1727
|
+
void internalEventRuntime.drain().catch(() => { }).finally(() => { drainingEvents = false; });
|
|
1678
1728
|
}, 1000) : undefined;
|
|
1679
1729
|
eventTimer?.unref();
|
|
1730
|
+
let externalSettled = true;
|
|
1731
|
+
if (externalEventRuntime)
|
|
1732
|
+
registerAppShutdownOwner(app, async (invocation) => {
|
|
1733
|
+
try {
|
|
1734
|
+
return externalSettled = await externalEventRuntime.settle(invocation);
|
|
1735
|
+
}
|
|
1736
|
+
catch {
|
|
1737
|
+
return externalSettled = false;
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1680
1740
|
app.addHook("onClose", async () => {
|
|
1681
1741
|
if (eventTimer)
|
|
1682
1742
|
clearInterval(eventTimer);
|
|
1743
|
+
const ownersSettled = await startAppShutdown(app);
|
|
1683
1744
|
await sdkHandler.close();
|
|
1745
|
+
if (!externalSettled)
|
|
1746
|
+
throw new Error("External Events shutdown has unfinished operations");
|
|
1747
|
+
if (!ownersSettled)
|
|
1748
|
+
throw new Error("APP_SHUTDOWN_INCOMPLETE");
|
|
1684
1749
|
});
|
|
1685
1750
|
runtimes.set(app, {
|
|
1686
1751
|
deployment,
|
|
@@ -1912,8 +1977,12 @@ export async function serveEmseepea(app, options = {}) {
|
|
|
1912
1977
|
return {
|
|
1913
1978
|
url: new URL(`http://${urlHost}:${address.port}/mcp`),
|
|
1914
1979
|
close: () => {
|
|
1980
|
+
if (closing)
|
|
1981
|
+
return closing;
|
|
1982
|
+
prepareAppShutdown(app, shutdownTimeoutMs);
|
|
1915
1983
|
runtime.stopping.abort();
|
|
1916
|
-
|
|
1984
|
+
void startAppShutdown(app);
|
|
1985
|
+
return closing = closeApp(app, remainingAppShutdown(app), observabilityFlushTimeoutMs, runtime.finishObservability);
|
|
1917
1986
|
},
|
|
1918
1987
|
};
|
|
1919
1988
|
}
|
|
@@ -2524,14 +2593,27 @@ function promptArguments(schema) {
|
|
|
2524
2593
|
})));
|
|
2525
2594
|
}
|
|
2526
2595
|
function sdkMetadataSchema(schema) {
|
|
2596
|
+
// The declaration is captured once, but the SDK reads metadata again for
|
|
2597
|
+
// subsequent registrations and requests. Keep output lazy: an input
|
|
2598
|
+
// transform can be valid even when it has no representable output schema.
|
|
2599
|
+
const documents = new Map();
|
|
2600
|
+
const document = (io) => {
|
|
2601
|
+
let captured = documents.get(io);
|
|
2602
|
+
if (!captured) {
|
|
2603
|
+
captured = deepFreeze(structuredClone(jsonMetadataSchema(schema, io)));
|
|
2604
|
+
documents.set(io, captured);
|
|
2605
|
+
}
|
|
2606
|
+
return captured;
|
|
2607
|
+
};
|
|
2608
|
+
document("input");
|
|
2527
2609
|
return {
|
|
2528
2610
|
"~standard": {
|
|
2529
2611
|
version: 1,
|
|
2530
2612
|
vendor: "emseepea",
|
|
2531
2613
|
validate: (value) => ({ value }),
|
|
2532
2614
|
jsonSchema: {
|
|
2533
|
-
input: () =>
|
|
2534
|
-
output: () =>
|
|
2615
|
+
input: () => document("input"),
|
|
2616
|
+
output: () => document("output"),
|
|
2535
2617
|
},
|
|
2536
2618
|
},
|
|
2537
2619
|
};
|
|
@@ -2702,7 +2784,12 @@ function requestId(value) {
|
|
|
2702
2784
|
async function runWithDeadline(requestSignal, deadlineMs, work) {
|
|
2703
2785
|
const remainingMs = deadlineMs - Date.now();
|
|
2704
2786
|
const deadlineSignal = remainingMs > 0 ? AbortSignal.timeout(remainingMs) : AbortSignal.abort();
|
|
2705
|
-
const
|
|
2787
|
+
const invocationSignal = requestOperations.getStore()?.signal;
|
|
2788
|
+
const signal = AbortSignal.any([
|
|
2789
|
+
requestSignal,
|
|
2790
|
+
deadlineSignal,
|
|
2791
|
+
...(invocationSignal ? [invocationSignal] : []),
|
|
2792
|
+
]);
|
|
2706
2793
|
return new Promise((resolve, reject) => {
|
|
2707
2794
|
const onAbort = () => reject(new Error("Tool execution cancelled"));
|
|
2708
2795
|
if (signal.aborted)
|
|
@@ -2968,13 +3055,19 @@ async function closeApp(app, timeoutMs, flushTimeoutMs, finishObservability) {
|
|
|
2968
3055
|
let onClosed;
|
|
2969
3056
|
const httpClosed = new Promise((resolve) => { onClosed = resolve; });
|
|
2970
3057
|
app.server.once("close", onClosed);
|
|
2971
|
-
const timeout = new Promise((
|
|
3058
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
2972
3059
|
timer = setTimeout(() => {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
3060
|
+
try {
|
|
3061
|
+
app.server.close();
|
|
3062
|
+
app.server.closeAllConnections();
|
|
3063
|
+
}
|
|
3064
|
+
catch {
|
|
3065
|
+
// Cleanup failure must not hide the incomplete shutdown outcome.
|
|
3066
|
+
}
|
|
3067
|
+
finally {
|
|
3068
|
+
reject(new Error("SERVER_SHUTDOWN_INCOMPLETE"));
|
|
3069
|
+
}
|
|
2976
3070
|
}, timeoutMs);
|
|
2977
|
-
timer.unref();
|
|
2978
3071
|
});
|
|
2979
3072
|
try {
|
|
2980
3073
|
await Promise.race([app.close(), timeout]);
|