@browserbridge/bbx 1.11.1 → 1.11.3
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/package.json +7 -7
- package/packages/agent-client/src/client.js +113 -39
- package/packages/agent-client/src/npm-self-update.js +7 -3
- package/packages/agent-client/src/runtime.js +4 -3
- package/packages/mcp-server/src/handlers-capture.js +1 -1
- package/packages/mcp-server/src/handlers-navigation.js +14 -21
- package/packages/mcp-server/src/handlers-page.js +16 -20
- package/packages/mcp-server/src/handlers-utils.js +188 -44
- package/packages/mcp-server/src/server.js +2 -1
- package/packages/native-host/src/auth-token.js +90 -28
- package/packages/native-host/src/daemon-process.js +5 -2
- package/packages/native-host/src/daemon.js +123 -23
- package/packages/native-host/src/framing.js +107 -17
- package/packages/native-host/src/native-host.js +4 -0
- package/packages/protocol/src/protocol.js +14 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { isProtocolVersionSupported } from '../../protocol/src/index.js';
|
|
2
3
|
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import net from 'node:net';
|
|
@@ -132,6 +133,7 @@ export const DAEMON_REGISTRATION_TIMEOUT_MS = 5_000;
|
|
|
132
133
|
export const MAX_DAEMON_CONNECTIONS = 64;
|
|
133
134
|
export const MAX_DAEMON_PENDING_REQUESTS = 128;
|
|
134
135
|
export const MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT = 16;
|
|
136
|
+
export const MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT = 32;
|
|
135
137
|
export const MAX_DAEMON_INVALID_LINES = 3;
|
|
136
138
|
const MAX_CLIENT_ID_LENGTH = 128;
|
|
137
139
|
const MAX_ROUTING_LABEL_LENGTH = 128;
|
|
@@ -360,7 +362,11 @@ function createHealthRecoveryPayload(daemon, extensionResult) {
|
|
|
360
362
|
function getVersionNegotiationPayload(requestedVersion) {
|
|
361
363
|
const supportedVersions = getSupportedProtocolVersions();
|
|
362
364
|
const latestSupported = supportedVersions[0];
|
|
363
|
-
if (
|
|
365
|
+
if (
|
|
366
|
+
!requestedVersion ||
|
|
367
|
+
!latestSupported ||
|
|
368
|
+
isProtocolVersionSupported(requestedVersion, supportedVersions)
|
|
369
|
+
) {
|
|
364
370
|
return { supported_versions: supportedVersions };
|
|
365
371
|
}
|
|
366
372
|
|
|
@@ -462,6 +468,11 @@ export class BridgeDaemon {
|
|
|
462
468
|
this.extensionSockets = new Map();
|
|
463
469
|
/** @type {Map<string, ClientSocket>} */
|
|
464
470
|
this.agentSockets = new Map();
|
|
471
|
+
/** @type {Set<ClientSocket>} */
|
|
472
|
+
this.clientSockets = new Set();
|
|
473
|
+
/** @type {Map<ClientSocket, number>} */
|
|
474
|
+
this.activeHandlers = new Map();
|
|
475
|
+
this.activeHandlerCount = 0;
|
|
465
476
|
/** @type {Map<string, PendingEntry>} */
|
|
466
477
|
this.pendingRequests = new Map();
|
|
467
478
|
/** @type {Map<ClientSocket, Set<string>>} */
|
|
@@ -568,7 +579,7 @@ export class BridgeDaemon {
|
|
|
568
579
|
pending = this.pendingRequests.get(requestId),
|
|
569
580
|
preserveArtifacts = false
|
|
570
581
|
) {
|
|
571
|
-
if (!pending) {
|
|
582
|
+
if (!pending || this.pendingRequests.get(requestId) !== pending) {
|
|
572
583
|
return undefined;
|
|
573
584
|
}
|
|
574
585
|
clearTimeout(pending.timeoutId);
|
|
@@ -621,6 +632,7 @@ export class BridgeDaemon {
|
|
|
621
632
|
* @returns {void}
|
|
622
633
|
*/
|
|
623
634
|
registerSocket(socket, message) {
|
|
635
|
+
this.clientSockets.add(socket);
|
|
624
636
|
if (socket.__role) {
|
|
625
637
|
void writeJsonLine(socket, {
|
|
626
638
|
type: 'registration_failed',
|
|
@@ -628,7 +640,7 @@ export class BridgeDaemon {
|
|
|
628
640
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
629
641
|
message: `Socket is already registered as ${socket.__role}.`,
|
|
630
642
|
},
|
|
631
|
-
});
|
|
643
|
+
}).catch(() => socket.destroy());
|
|
632
644
|
return;
|
|
633
645
|
}
|
|
634
646
|
|
|
@@ -639,7 +651,7 @@ export class BridgeDaemon {
|
|
|
639
651
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
640
652
|
message: 'Socket role must be "agent" or "extension".',
|
|
641
653
|
},
|
|
642
|
-
});
|
|
654
|
+
}).catch(() => socket.destroy());
|
|
643
655
|
return;
|
|
644
656
|
}
|
|
645
657
|
|
|
@@ -657,7 +669,9 @@ export class BridgeDaemon {
|
|
|
657
669
|
code: ERROR_CODES.ACCESS_DENIED,
|
|
658
670
|
message: 'Bridge daemon authentication failed.',
|
|
659
671
|
},
|
|
660
|
-
})
|
|
672
|
+
})
|
|
673
|
+
.catch(() => {})
|
|
674
|
+
.finally(() => socket.destroy());
|
|
661
675
|
return;
|
|
662
676
|
}
|
|
663
677
|
|
|
@@ -686,7 +700,9 @@ export class BridgeDaemon {
|
|
|
686
700
|
browserName: socket.__browserName ?? null,
|
|
687
701
|
profileLabel: socket.__profileLabel ?? null,
|
|
688
702
|
});
|
|
689
|
-
void writeJsonLine(socket, { type: 'registered', role: 'extension' })
|
|
703
|
+
void writeJsonLine(socket, { type: 'registered', role: 'extension' }).catch(() =>
|
|
704
|
+
socket.destroy()
|
|
705
|
+
);
|
|
690
706
|
return;
|
|
691
707
|
}
|
|
692
708
|
|
|
@@ -700,7 +716,9 @@ export class BridgeDaemon {
|
|
|
700
716
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
701
717
|
message: 'Agent clientId is invalid.',
|
|
702
718
|
},
|
|
703
|
-
})
|
|
719
|
+
})
|
|
720
|
+
.catch(() => {})
|
|
721
|
+
.finally(() => socket.destroy());
|
|
704
722
|
return;
|
|
705
723
|
}
|
|
706
724
|
this.agentSockets.set(clientId, socket);
|
|
@@ -710,7 +728,7 @@ export class BridgeDaemon {
|
|
|
710
728
|
type: 'registered',
|
|
711
729
|
role: 'agent',
|
|
712
730
|
clientId,
|
|
713
|
-
});
|
|
731
|
+
}).catch(() => socket.destroy());
|
|
714
732
|
return;
|
|
715
733
|
}
|
|
716
734
|
}
|
|
@@ -734,7 +752,7 @@ export class BridgeDaemon {
|
|
|
734
752
|
}
|
|
735
753
|
try {
|
|
736
754
|
await fs.promises.access(this.socketPath);
|
|
737
|
-
if (await
|
|
755
|
+
if (await hasLiveListener(this.transport)) {
|
|
738
756
|
throw new Error(
|
|
739
757
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
740
758
|
);
|
|
@@ -742,17 +760,17 @@ export class BridgeDaemon {
|
|
|
742
760
|
this.logger.info('Removing stale socket from previous run', {
|
|
743
761
|
socketPath: this.socketPath,
|
|
744
762
|
});
|
|
763
|
+
await fs.promises.rm(this.socketPath, { force: true });
|
|
745
764
|
} catch (error) {
|
|
746
|
-
if (error
|
|
765
|
+
if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
|
|
747
766
|
throw error;
|
|
748
|
-
}
|
|
749
767
|
// Socket does not exist - normal startup.
|
|
750
768
|
}
|
|
751
|
-
await fs.promises.rm(this.socketPath, { force: true });
|
|
752
769
|
}
|
|
753
770
|
|
|
754
771
|
this.server = net.createServer((socket) => {
|
|
755
772
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
773
|
+
this.clientSockets.add(typedSocket);
|
|
756
774
|
typedSocket.__invalidLineCount = 0;
|
|
757
775
|
typedSocket.__registrationTimeoutId = setTimeout(() => {
|
|
758
776
|
if (!typedSocket.__role) {
|
|
@@ -767,12 +785,37 @@ export class BridgeDaemon {
|
|
|
767
785
|
parseJsonLines(
|
|
768
786
|
typedSocket,
|
|
769
787
|
(raw) => {
|
|
788
|
+
if (typedSocket.destroyed) return;
|
|
770
789
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
790
|
+
const admitted =
|
|
791
|
+
message?.type === 'agent.request' || message?.type === 'extension.setup_status.request';
|
|
792
|
+
const active = this.activeHandlers.get(typedSocket) ?? 0;
|
|
793
|
+
if (
|
|
794
|
+
admitted &&
|
|
795
|
+
(active >= MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT ||
|
|
796
|
+
this.activeHandlerCount >= MAX_DAEMON_PENDING_REQUESTS)
|
|
797
|
+
) {
|
|
798
|
+
typedSocket.destroy();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (admitted) {
|
|
802
|
+
this.activeHandlers.set(typedSocket, active + 1);
|
|
803
|
+
this.activeHandlerCount += 1;
|
|
804
|
+
}
|
|
805
|
+
void this.handleClientMessage(typedSocket, message)
|
|
806
|
+
.catch((err) => {
|
|
807
|
+
this.logger.error('handler error', {
|
|
808
|
+
message: err instanceof Error ? err.message : String(err),
|
|
809
|
+
});
|
|
810
|
+
})
|
|
811
|
+
.finally(() => {
|
|
812
|
+
if (admitted) {
|
|
813
|
+
const remaining = (this.activeHandlers.get(typedSocket) ?? 1) - 1;
|
|
814
|
+
if (remaining) this.activeHandlers.set(typedSocket, remaining);
|
|
815
|
+
else this.activeHandlers.delete(typedSocket);
|
|
816
|
+
this.activeHandlerCount -= 1;
|
|
817
|
+
}
|
|
774
818
|
});
|
|
775
|
-
});
|
|
776
819
|
},
|
|
777
820
|
{
|
|
778
821
|
onProtocolError: (error) => {
|
|
@@ -845,6 +888,10 @@ export class BridgeDaemon {
|
|
|
845
888
|
this.pendingRequests.clear();
|
|
846
889
|
this.pendingRequestsByOwnerSocket.clear();
|
|
847
890
|
this.pendingRequestsByTargetSocket.clear();
|
|
891
|
+
this.requestStartTimes.clear();
|
|
892
|
+
|
|
893
|
+
for (const socket of this.clientSockets) socket.destroy();
|
|
894
|
+
this.clientSockets.clear();
|
|
848
895
|
|
|
849
896
|
for (const socket of this.agentSockets.values()) {
|
|
850
897
|
socket.destroy();
|
|
@@ -864,6 +911,7 @@ export class BridgeDaemon {
|
|
|
864
911
|
|
|
865
912
|
if (this.server) {
|
|
866
913
|
const server = this.server;
|
|
914
|
+
const ownedListener = server.listening;
|
|
867
915
|
this.server = null;
|
|
868
916
|
try {
|
|
869
917
|
await new Promise((resolve, reject) => {
|
|
@@ -876,7 +924,11 @@ export class BridgeDaemon {
|
|
|
876
924
|
});
|
|
877
925
|
});
|
|
878
926
|
} finally {
|
|
879
|
-
if (
|
|
927
|
+
if (
|
|
928
|
+
ownedListener &&
|
|
929
|
+
this.transport.type === 'socket' &&
|
|
930
|
+
!isWindowsNamedPipePath(this.socketPath)
|
|
931
|
+
) {
|
|
880
932
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
881
933
|
}
|
|
882
934
|
}
|
|
@@ -1321,7 +1373,7 @@ export class BridgeDaemon {
|
|
|
1321
1373
|
message: error instanceof Error ? error.message : String(error),
|
|
1322
1374
|
});
|
|
1323
1375
|
const pending = this.pendingRequests.get(request.id);
|
|
1324
|
-
if (!pending) {
|
|
1376
|
+
if (!pending || !pending.targets.has(target)) {
|
|
1325
1377
|
return;
|
|
1326
1378
|
}
|
|
1327
1379
|
this.removePendingTarget(request.id, pending, target);
|
|
@@ -1496,6 +1548,20 @@ export class BridgeDaemon {
|
|
|
1496
1548
|
const ownerId = pending.socket.__clientId;
|
|
1497
1549
|
if (!ownerId) return;
|
|
1498
1550
|
try {
|
|
1551
|
+
if (
|
|
1552
|
+
message.type === 'extension.artifact.chunk' ||
|
|
1553
|
+
message.type === 'extension.artifact.commit'
|
|
1554
|
+
) {
|
|
1555
|
+
const transfer = this.artifactStore.transfers.get(String(message.artifactId ?? ''));
|
|
1556
|
+
if (
|
|
1557
|
+
!transfer ||
|
|
1558
|
+
transfer.requestId !== requestId ||
|
|
1559
|
+
transfer.ownerId !== ownerId ||
|
|
1560
|
+
transfer.extensionId !== socket.__extensionId
|
|
1561
|
+
) {
|
|
1562
|
+
throw new Error('Artifact transfer does not belong to this request and extension.');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1499
1565
|
if (message.type === 'extension.artifact.begin') {
|
|
1500
1566
|
this.artifactStore.begin({
|
|
1501
1567
|
artifactId: String(artifact.artifactId ?? ''),
|
|
@@ -1591,6 +1657,7 @@ export class BridgeDaemon {
|
|
|
1591
1657
|
}
|
|
1592
1658
|
|
|
1593
1659
|
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
1660
|
+
clearTimeout(pending.timeoutId);
|
|
1594
1661
|
|
|
1595
1662
|
if (responseMessage.ok) {
|
|
1596
1663
|
const result =
|
|
@@ -1636,7 +1703,7 @@ export class BridgeDaemon {
|
|
|
1636
1703
|
}
|
|
1637
1704
|
}
|
|
1638
1705
|
if (!this.domBaselines.register(baselineId, socket, result.expiresAt)) {
|
|
1639
|
-
await this.releaseOrphanDomBaseline(socket, responseMessage);
|
|
1706
|
+
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1640
1707
|
pending.lastErrorResponse = createFailure(
|
|
1641
1708
|
responseMessage.id,
|
|
1642
1709
|
ERROR_CODES.DOM_BASELINE_INVALIDATED,
|
|
@@ -1688,11 +1755,12 @@ export class BridgeDaemon {
|
|
|
1688
1755
|
if (baselineId) this.domBaselines.delete(baselineId);
|
|
1689
1756
|
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1690
1757
|
}
|
|
1691
|
-
this.clearPendingRequest(responseMessage.id, pending)
|
|
1692
|
-
|
|
1758
|
+
if (this.clearPendingRequest(responseMessage.id, pending)) {
|
|
1759
|
+
this.recordRequestCompletion(responseMessage.id, false, pending);
|
|
1760
|
+
}
|
|
1693
1761
|
throw error;
|
|
1694
1762
|
}
|
|
1695
|
-
this.clearPendingRequest(responseMessage.id, pending, true);
|
|
1763
|
+
if (!this.clearPendingRequest(responseMessage.id, pending, true)) return;
|
|
1696
1764
|
this.recordRequestCompletion(responseMessage.id, true, pending);
|
|
1697
1765
|
this.pushLog({
|
|
1698
1766
|
at: new Date().toISOString(),
|
|
@@ -1719,6 +1787,7 @@ export class BridgeDaemon {
|
|
|
1719
1787
|
* @returns {void}
|
|
1720
1788
|
*/
|
|
1721
1789
|
handleSocketClose(socket) {
|
|
1790
|
+
this.clientSockets.delete(socket);
|
|
1722
1791
|
if (socket.__registrationTimeoutId) {
|
|
1723
1792
|
clearTimeout(socket.__registrationTimeoutId);
|
|
1724
1793
|
socket.__registrationTimeoutId = undefined;
|
|
@@ -1781,7 +1850,7 @@ export class BridgeDaemon {
|
|
|
1781
1850
|
* @returns {Promise<void>}
|
|
1782
1851
|
*/
|
|
1783
1852
|
async finishPendingRequestIfExhausted(requestId, pending) {
|
|
1784
|
-
if (pending.targets.size > 0 ||
|
|
1853
|
+
if (pending.targets.size > 0 || this.pendingRequests.get(requestId) !== pending) {
|
|
1785
1854
|
return;
|
|
1786
1855
|
}
|
|
1787
1856
|
|
|
@@ -1975,6 +2044,37 @@ export async function pingExistingDaemon(transport) {
|
|
|
1975
2044
|
});
|
|
1976
2045
|
}
|
|
1977
2046
|
|
|
2047
|
+
/**
|
|
2048
|
+
* Only a refused or missing endpoint proves a Unix socket is stale.
|
|
2049
|
+
* @param {BridgeTransport} transport
|
|
2050
|
+
* @returns {Promise<boolean>}
|
|
2051
|
+
*/
|
|
2052
|
+
export function hasLiveListener(transport) {
|
|
2053
|
+
return new Promise((resolve, reject) => {
|
|
2054
|
+
const socket =
|
|
2055
|
+
transport.type === 'tcp'
|
|
2056
|
+
? net.createConnection({ host: transport.host, port: transport.port })
|
|
2057
|
+
: net.createConnection(transport.socketPath);
|
|
2058
|
+
const timer = setTimeout(() => finish(true), DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
2059
|
+
/** @param {boolean} live @param {Error} [error] */
|
|
2060
|
+
function finish(live, error) {
|
|
2061
|
+
clearTimeout(timer);
|
|
2062
|
+
socket.destroy();
|
|
2063
|
+
if (error) reject(error);
|
|
2064
|
+
else resolve(live);
|
|
2065
|
+
}
|
|
2066
|
+
socket.once('connect', () => finish(true));
|
|
2067
|
+
socket.once('error', (error) => {
|
|
2068
|
+
if (
|
|
2069
|
+
'code' in error &&
|
|
2070
|
+
(error.code === 'ENOENT' || error.code === 'ECONNREFUSED' || error.code === 'ENOTSOCK')
|
|
2071
|
+
)
|
|
2072
|
+
finish(false);
|
|
2073
|
+
else finish(true, error);
|
|
2074
|
+
});
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
|
|
1978
2078
|
/**
|
|
1979
2079
|
* @param {Record<string, unknown>} params
|
|
1980
2080
|
* @returns {SetupInstallParams & { action: 'install' | 'uninstall', kind: 'mcp' | 'skill', target: string }}
|
|
@@ -1,9 +1,111 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { once } from 'node:events';
|
|
4
|
-
|
|
5
3
|
import { MAX_JSON_LINE_BYTES, MAX_NATIVE_MESSAGE_BYTES } from '../../protocol/src/index.js';
|
|
6
4
|
|
|
5
|
+
export const MAX_OUTPUT_QUEUE_BYTES = 4 * MAX_JSON_LINE_BYTES;
|
|
6
|
+
export const MAX_OUTPUT_QUEUE_MESSAGES = 128;
|
|
7
|
+
export const OUTPUT_DRAIN_TIMEOUT_MS = 30_000;
|
|
8
|
+
|
|
9
|
+
/** @typedef {NodeJS.WritableStream & { destroyed?: boolean, writableEnded?: boolean, destroy?: () => unknown }} OutputStream */
|
|
10
|
+
/** @typedef {{ parts: Array<string | Buffer>, bytes: number, resolve: () => void, reject: (error: Error) => void }} QueuedWrite */
|
|
11
|
+
|
|
12
|
+
/** @param {OutputStream} stream @returns {(parts: Array<string | Buffer>) => Promise<void>} */
|
|
13
|
+
function createBoundedWriter(stream) {
|
|
14
|
+
/** @type {QueuedWrite[]} */
|
|
15
|
+
const queue = [];
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
let running = false;
|
|
18
|
+
/** @type {Error | null} */
|
|
19
|
+
let failure = null;
|
|
20
|
+
/** @type {((error: Error) => void) | null} */
|
|
21
|
+
let cancelDrain = null;
|
|
22
|
+
|
|
23
|
+
/** @param {Error} error */
|
|
24
|
+
function fail(error) {
|
|
25
|
+
failure ??= error;
|
|
26
|
+
cancelDrain?.(error);
|
|
27
|
+
for (const entry of queue.splice(0)) entry.reject(error);
|
|
28
|
+
bytes = 0;
|
|
29
|
+
stream.destroy?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function pump() {
|
|
33
|
+
if (running) return;
|
|
34
|
+
running = true;
|
|
35
|
+
try {
|
|
36
|
+
while (queue.length && !failure) {
|
|
37
|
+
const entry = queue[0];
|
|
38
|
+
for (const part of entry.parts) {
|
|
39
|
+
if (failure) throw failure;
|
|
40
|
+
if (stream.destroyed || stream.writableEnded) throw new Error('Output stream closed.');
|
|
41
|
+
if (!stream.write(part)) {
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
const timer = setTimeout(
|
|
44
|
+
() => finish(new Error('Output drain timed out.')),
|
|
45
|
+
OUTPUT_DRAIN_TIMEOUT_MS
|
|
46
|
+
);
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
/** @param {Error} [error] */
|
|
49
|
+
function finish(error) {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
stream.removeListener('drain', drained);
|
|
52
|
+
stream.removeListener('close', closed);
|
|
53
|
+
stream.removeListener('error', finish);
|
|
54
|
+
cancelDrain = null;
|
|
55
|
+
if (error) reject(error);
|
|
56
|
+
else resolve(undefined);
|
|
57
|
+
}
|
|
58
|
+
const drained = () => finish();
|
|
59
|
+
const closed = () => finish(new Error('Output stream closed while writing.'));
|
|
60
|
+
cancelDrain = finish;
|
|
61
|
+
stream.once('drain', drained);
|
|
62
|
+
stream.once('close', closed);
|
|
63
|
+
stream.once('error', finish);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (failure) break;
|
|
68
|
+
queue.shift();
|
|
69
|
+
bytes -= entry.bytes;
|
|
70
|
+
entry.resolve();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
74
|
+
} finally {
|
|
75
|
+
running = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (parts) => {
|
|
80
|
+
if (failure) return Promise.reject(failure);
|
|
81
|
+
const size = parts.reduce((total, part) => total + Buffer.byteLength(part), 0);
|
|
82
|
+
if (queue.length >= MAX_OUTPUT_QUEUE_MESSAGES || bytes + size > MAX_OUTPUT_QUEUE_BYTES) {
|
|
83
|
+
const error = new Error('Output queue limit exceeded.');
|
|
84
|
+
fail(error);
|
|
85
|
+
return Promise.reject(error);
|
|
86
|
+
}
|
|
87
|
+
const result = new Promise((resolve, reject) => {
|
|
88
|
+
queue.push({ parts, bytes: size, resolve: () => resolve(undefined), reject });
|
|
89
|
+
bytes += size;
|
|
90
|
+
});
|
|
91
|
+
void pump();
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @type {WeakMap<OutputStream, ReturnType<typeof createBoundedWriter>>} */
|
|
97
|
+
const outputWriters = new WeakMap();
|
|
98
|
+
|
|
99
|
+
/** @param {OutputStream} stream @param {Array<string | Buffer>} parts */
|
|
100
|
+
function writeParts(stream, parts) {
|
|
101
|
+
let writer = outputWriters.get(stream);
|
|
102
|
+
if (!writer) {
|
|
103
|
+
writer = createBoundedWriter(stream);
|
|
104
|
+
outputWriters.set(stream, writer);
|
|
105
|
+
}
|
|
106
|
+
return writer(parts);
|
|
107
|
+
}
|
|
108
|
+
|
|
7
109
|
/**
|
|
8
110
|
* @param {NodeJS.WritableStream} stream
|
|
9
111
|
* @param {unknown} message
|
|
@@ -16,12 +118,7 @@ export async function writeNativeMessage(stream, message) {
|
|
|
16
118
|
}
|
|
17
119
|
const header = Buffer.alloc(4);
|
|
18
120
|
header.writeUInt32LE(payload.length, 0);
|
|
19
|
-
|
|
20
|
-
await once(stream, 'drain');
|
|
21
|
-
}
|
|
22
|
-
if (!stream.write(payload)) {
|
|
23
|
-
await once(stream, 'drain');
|
|
24
|
-
}
|
|
121
|
+
await writeParts(stream, [header, payload]);
|
|
25
122
|
}
|
|
26
123
|
|
|
27
124
|
/**
|
|
@@ -29,12 +126,7 @@ export async function writeNativeMessage(stream, message) {
|
|
|
29
126
|
* @returns {(message: unknown) => Promise<void>}
|
|
30
127
|
*/
|
|
31
128
|
export function createNativeMessageWriter(stream) {
|
|
32
|
-
|
|
33
|
-
return (message) => {
|
|
34
|
-
const writePromise = queue.then(() => writeNativeMessage(stream, message));
|
|
35
|
-
queue = writePromise.catch(() => {});
|
|
36
|
-
return writePromise;
|
|
37
|
-
};
|
|
129
|
+
return (message) => writeNativeMessage(stream, message);
|
|
38
130
|
}
|
|
39
131
|
|
|
40
132
|
/**
|
|
@@ -198,7 +290,5 @@ export async function writeJsonLine(socket, message) {
|
|
|
198
290
|
if (byteLength > MAX_JSON_LINE_BYTES) {
|
|
199
291
|
throw new Error(`JSON line exceeds ${MAX_JSON_LINE_BYTES} bytes: ${byteLength}`);
|
|
200
292
|
}
|
|
201
|
-
|
|
202
|
-
await once(socket, 'drain');
|
|
203
|
-
}
|
|
293
|
+
await writeParts(socket, [line]);
|
|
204
294
|
}
|
|
@@ -173,6 +173,7 @@ export async function runNativeHost({
|
|
|
173
173
|
parseJsonLines(
|
|
174
174
|
socket,
|
|
175
175
|
(raw) => {
|
|
176
|
+
if (socket.destroyed) return;
|
|
176
177
|
const message = /** @type {Record<string, unknown>} */ (raw);
|
|
177
178
|
void (async () => {
|
|
178
179
|
if (message.type === 'extension.request') {
|
|
@@ -201,6 +202,7 @@ export async function runNativeHost({
|
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
})().catch((err) => {
|
|
205
|
+
socket.destroy();
|
|
204
206
|
console.error(
|
|
205
207
|
'native-host: socket message handler failed:',
|
|
206
208
|
err instanceof Error ? err.message : err
|
|
@@ -220,6 +222,7 @@ export async function runNativeHost({
|
|
|
220
222
|
createNativeMessageReader(
|
|
221
223
|
process.stdin,
|
|
222
224
|
(message) => {
|
|
225
|
+
if (socket.destroyed) return;
|
|
223
226
|
void (async () => {
|
|
224
227
|
if (isHostBridgeRequest(message)) {
|
|
225
228
|
await writeJsonLine(socket, {
|
|
@@ -271,6 +274,7 @@ export async function runNativeHost({
|
|
|
271
274
|
response: message,
|
|
272
275
|
});
|
|
273
276
|
})().catch((err) => {
|
|
277
|
+
socket.destroy();
|
|
274
278
|
console.error(
|
|
275
279
|
'native-host: stdin message handler failed:',
|
|
276
280
|
err instanceof Error ? err.message : err
|
|
@@ -133,6 +133,20 @@ export function deriveProtocolVersion(packageVersion) {
|
|
|
133
133
|
return match ? `${match[1]}.${match[2]}` : '0.0';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Minor and patch releases are compatible in either direction within a major.
|
|
138
|
+
* @param {string} requestedVersion
|
|
139
|
+
* @param {readonly string[]} supportedVersions
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
export function isProtocolVersionSupported(requestedVersion, supportedVersions) {
|
|
143
|
+
const major = /^(\d+)\.\d+$/.exec(requestedVersion)?.[1];
|
|
144
|
+
return (
|
|
145
|
+
major !== undefined &&
|
|
146
|
+
supportedVersions.some((version) => /^(\d+)\.\d+$/.exec(version)?.[1] === major)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
/**
|
|
137
151
|
* @returns {string | null}
|
|
138
152
|
*/
|