@browserbridge/bbx 1.11.1 → 1.11.2
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 +1 -1
- package/packages/agent-client/src/client.js +110 -37
- package/packages/agent-client/src/runtime.js +1 -1
- 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 +117 -22
- package/packages/native-host/src/framing.js +107 -17
- package/packages/native-host/src/native-host.js +4 -0
|
@@ -132,6 +132,7 @@ export const DAEMON_REGISTRATION_TIMEOUT_MS = 5_000;
|
|
|
132
132
|
export const MAX_DAEMON_CONNECTIONS = 64;
|
|
133
133
|
export const MAX_DAEMON_PENDING_REQUESTS = 128;
|
|
134
134
|
export const MAX_DAEMON_PENDING_REQUESTS_PER_CLIENT = 16;
|
|
135
|
+
export const MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT = 32;
|
|
135
136
|
export const MAX_DAEMON_INVALID_LINES = 3;
|
|
136
137
|
const MAX_CLIENT_ID_LENGTH = 128;
|
|
137
138
|
const MAX_ROUTING_LABEL_LENGTH = 128;
|
|
@@ -462,6 +463,11 @@ export class BridgeDaemon {
|
|
|
462
463
|
this.extensionSockets = new Map();
|
|
463
464
|
/** @type {Map<string, ClientSocket>} */
|
|
464
465
|
this.agentSockets = new Map();
|
|
466
|
+
/** @type {Set<ClientSocket>} */
|
|
467
|
+
this.clientSockets = new Set();
|
|
468
|
+
/** @type {Map<ClientSocket, number>} */
|
|
469
|
+
this.activeHandlers = new Map();
|
|
470
|
+
this.activeHandlerCount = 0;
|
|
465
471
|
/** @type {Map<string, PendingEntry>} */
|
|
466
472
|
this.pendingRequests = new Map();
|
|
467
473
|
/** @type {Map<ClientSocket, Set<string>>} */
|
|
@@ -568,7 +574,7 @@ export class BridgeDaemon {
|
|
|
568
574
|
pending = this.pendingRequests.get(requestId),
|
|
569
575
|
preserveArtifacts = false
|
|
570
576
|
) {
|
|
571
|
-
if (!pending) {
|
|
577
|
+
if (!pending || this.pendingRequests.get(requestId) !== pending) {
|
|
572
578
|
return undefined;
|
|
573
579
|
}
|
|
574
580
|
clearTimeout(pending.timeoutId);
|
|
@@ -621,6 +627,7 @@ export class BridgeDaemon {
|
|
|
621
627
|
* @returns {void}
|
|
622
628
|
*/
|
|
623
629
|
registerSocket(socket, message) {
|
|
630
|
+
this.clientSockets.add(socket);
|
|
624
631
|
if (socket.__role) {
|
|
625
632
|
void writeJsonLine(socket, {
|
|
626
633
|
type: 'registration_failed',
|
|
@@ -628,7 +635,7 @@ export class BridgeDaemon {
|
|
|
628
635
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
629
636
|
message: `Socket is already registered as ${socket.__role}.`,
|
|
630
637
|
},
|
|
631
|
-
});
|
|
638
|
+
}).catch(() => socket.destroy());
|
|
632
639
|
return;
|
|
633
640
|
}
|
|
634
641
|
|
|
@@ -639,7 +646,7 @@ export class BridgeDaemon {
|
|
|
639
646
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
640
647
|
message: 'Socket role must be "agent" or "extension".',
|
|
641
648
|
},
|
|
642
|
-
});
|
|
649
|
+
}).catch(() => socket.destroy());
|
|
643
650
|
return;
|
|
644
651
|
}
|
|
645
652
|
|
|
@@ -657,7 +664,9 @@ export class BridgeDaemon {
|
|
|
657
664
|
code: ERROR_CODES.ACCESS_DENIED,
|
|
658
665
|
message: 'Bridge daemon authentication failed.',
|
|
659
666
|
},
|
|
660
|
-
})
|
|
667
|
+
})
|
|
668
|
+
.catch(() => {})
|
|
669
|
+
.finally(() => socket.destroy());
|
|
661
670
|
return;
|
|
662
671
|
}
|
|
663
672
|
|
|
@@ -686,7 +695,9 @@ export class BridgeDaemon {
|
|
|
686
695
|
browserName: socket.__browserName ?? null,
|
|
687
696
|
profileLabel: socket.__profileLabel ?? null,
|
|
688
697
|
});
|
|
689
|
-
void writeJsonLine(socket, { type: 'registered', role: 'extension' })
|
|
698
|
+
void writeJsonLine(socket, { type: 'registered', role: 'extension' }).catch(() =>
|
|
699
|
+
socket.destroy()
|
|
700
|
+
);
|
|
690
701
|
return;
|
|
691
702
|
}
|
|
692
703
|
|
|
@@ -700,7 +711,9 @@ export class BridgeDaemon {
|
|
|
700
711
|
code: ERROR_CODES.INVALID_REQUEST,
|
|
701
712
|
message: 'Agent clientId is invalid.',
|
|
702
713
|
},
|
|
703
|
-
})
|
|
714
|
+
})
|
|
715
|
+
.catch(() => {})
|
|
716
|
+
.finally(() => socket.destroy());
|
|
704
717
|
return;
|
|
705
718
|
}
|
|
706
719
|
this.agentSockets.set(clientId, socket);
|
|
@@ -710,7 +723,7 @@ export class BridgeDaemon {
|
|
|
710
723
|
type: 'registered',
|
|
711
724
|
role: 'agent',
|
|
712
725
|
clientId,
|
|
713
|
-
});
|
|
726
|
+
}).catch(() => socket.destroy());
|
|
714
727
|
return;
|
|
715
728
|
}
|
|
716
729
|
}
|
|
@@ -734,7 +747,7 @@ export class BridgeDaemon {
|
|
|
734
747
|
}
|
|
735
748
|
try {
|
|
736
749
|
await fs.promises.access(this.socketPath);
|
|
737
|
-
if (await
|
|
750
|
+
if (await hasLiveListener(this.transport)) {
|
|
738
751
|
throw new Error(
|
|
739
752
|
`Another daemon is already running on ${this.socketPath}. Stop it before starting a new one.`
|
|
740
753
|
);
|
|
@@ -742,17 +755,17 @@ export class BridgeDaemon {
|
|
|
742
755
|
this.logger.info('Removing stale socket from previous run', {
|
|
743
756
|
socketPath: this.socketPath,
|
|
744
757
|
});
|
|
758
|
+
await fs.promises.rm(this.socketPath, { force: true });
|
|
745
759
|
} catch (error) {
|
|
746
|
-
if (error
|
|
760
|
+
if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
|
|
747
761
|
throw error;
|
|
748
|
-
}
|
|
749
762
|
// Socket does not exist - normal startup.
|
|
750
763
|
}
|
|
751
|
-
await fs.promises.rm(this.socketPath, { force: true });
|
|
752
764
|
}
|
|
753
765
|
|
|
754
766
|
this.server = net.createServer((socket) => {
|
|
755
767
|
const typedSocket = /** @type {ClientSocket} */ (socket);
|
|
768
|
+
this.clientSockets.add(typedSocket);
|
|
756
769
|
typedSocket.__invalidLineCount = 0;
|
|
757
770
|
typedSocket.__registrationTimeoutId = setTimeout(() => {
|
|
758
771
|
if (!typedSocket.__role) {
|
|
@@ -767,12 +780,37 @@ export class BridgeDaemon {
|
|
|
767
780
|
parseJsonLines(
|
|
768
781
|
typedSocket,
|
|
769
782
|
(raw) => {
|
|
783
|
+
if (typedSocket.destroyed) return;
|
|
770
784
|
const message = /** @type {DaemonMessage} */ (raw);
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
785
|
+
const admitted =
|
|
786
|
+
message?.type === 'agent.request' || message?.type === 'extension.setup_status.request';
|
|
787
|
+
const active = this.activeHandlers.get(typedSocket) ?? 0;
|
|
788
|
+
if (
|
|
789
|
+
admitted &&
|
|
790
|
+
(active >= MAX_DAEMON_ACTIVE_HANDLERS_PER_CLIENT ||
|
|
791
|
+
this.activeHandlerCount >= MAX_DAEMON_PENDING_REQUESTS)
|
|
792
|
+
) {
|
|
793
|
+
typedSocket.destroy();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (admitted) {
|
|
797
|
+
this.activeHandlers.set(typedSocket, active + 1);
|
|
798
|
+
this.activeHandlerCount += 1;
|
|
799
|
+
}
|
|
800
|
+
void this.handleClientMessage(typedSocket, message)
|
|
801
|
+
.catch((err) => {
|
|
802
|
+
this.logger.error('handler error', {
|
|
803
|
+
message: err instanceof Error ? err.message : String(err),
|
|
804
|
+
});
|
|
805
|
+
})
|
|
806
|
+
.finally(() => {
|
|
807
|
+
if (admitted) {
|
|
808
|
+
const remaining = (this.activeHandlers.get(typedSocket) ?? 1) - 1;
|
|
809
|
+
if (remaining) this.activeHandlers.set(typedSocket, remaining);
|
|
810
|
+
else this.activeHandlers.delete(typedSocket);
|
|
811
|
+
this.activeHandlerCount -= 1;
|
|
812
|
+
}
|
|
774
813
|
});
|
|
775
|
-
});
|
|
776
814
|
},
|
|
777
815
|
{
|
|
778
816
|
onProtocolError: (error) => {
|
|
@@ -845,6 +883,10 @@ export class BridgeDaemon {
|
|
|
845
883
|
this.pendingRequests.clear();
|
|
846
884
|
this.pendingRequestsByOwnerSocket.clear();
|
|
847
885
|
this.pendingRequestsByTargetSocket.clear();
|
|
886
|
+
this.requestStartTimes.clear();
|
|
887
|
+
|
|
888
|
+
for (const socket of this.clientSockets) socket.destroy();
|
|
889
|
+
this.clientSockets.clear();
|
|
848
890
|
|
|
849
891
|
for (const socket of this.agentSockets.values()) {
|
|
850
892
|
socket.destroy();
|
|
@@ -864,6 +906,7 @@ export class BridgeDaemon {
|
|
|
864
906
|
|
|
865
907
|
if (this.server) {
|
|
866
908
|
const server = this.server;
|
|
909
|
+
const ownedListener = server.listening;
|
|
867
910
|
this.server = null;
|
|
868
911
|
try {
|
|
869
912
|
await new Promise((resolve, reject) => {
|
|
@@ -876,7 +919,11 @@ export class BridgeDaemon {
|
|
|
876
919
|
});
|
|
877
920
|
});
|
|
878
921
|
} finally {
|
|
879
|
-
if (
|
|
922
|
+
if (
|
|
923
|
+
ownedListener &&
|
|
924
|
+
this.transport.type === 'socket' &&
|
|
925
|
+
!isWindowsNamedPipePath(this.socketPath)
|
|
926
|
+
) {
|
|
880
927
|
await fs.promises.rm(this.socketPath, { force: true });
|
|
881
928
|
}
|
|
882
929
|
}
|
|
@@ -1321,7 +1368,7 @@ export class BridgeDaemon {
|
|
|
1321
1368
|
message: error instanceof Error ? error.message : String(error),
|
|
1322
1369
|
});
|
|
1323
1370
|
const pending = this.pendingRequests.get(request.id);
|
|
1324
|
-
if (!pending) {
|
|
1371
|
+
if (!pending || !pending.targets.has(target)) {
|
|
1325
1372
|
return;
|
|
1326
1373
|
}
|
|
1327
1374
|
this.removePendingTarget(request.id, pending, target);
|
|
@@ -1496,6 +1543,20 @@ export class BridgeDaemon {
|
|
|
1496
1543
|
const ownerId = pending.socket.__clientId;
|
|
1497
1544
|
if (!ownerId) return;
|
|
1498
1545
|
try {
|
|
1546
|
+
if (
|
|
1547
|
+
message.type === 'extension.artifact.chunk' ||
|
|
1548
|
+
message.type === 'extension.artifact.commit'
|
|
1549
|
+
) {
|
|
1550
|
+
const transfer = this.artifactStore.transfers.get(String(message.artifactId ?? ''));
|
|
1551
|
+
if (
|
|
1552
|
+
!transfer ||
|
|
1553
|
+
transfer.requestId !== requestId ||
|
|
1554
|
+
transfer.ownerId !== ownerId ||
|
|
1555
|
+
transfer.extensionId !== socket.__extensionId
|
|
1556
|
+
) {
|
|
1557
|
+
throw new Error('Artifact transfer does not belong to this request and extension.');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1499
1560
|
if (message.type === 'extension.artifact.begin') {
|
|
1500
1561
|
this.artifactStore.begin({
|
|
1501
1562
|
artifactId: String(artifact.artifactId ?? ''),
|
|
@@ -1591,6 +1652,7 @@ export class BridgeDaemon {
|
|
|
1591
1652
|
}
|
|
1592
1653
|
|
|
1593
1654
|
this.removePendingTarget(responseMessage.id, pending, socket);
|
|
1655
|
+
clearTimeout(pending.timeoutId);
|
|
1594
1656
|
|
|
1595
1657
|
if (responseMessage.ok) {
|
|
1596
1658
|
const result =
|
|
@@ -1636,7 +1698,7 @@ export class BridgeDaemon {
|
|
|
1636
1698
|
}
|
|
1637
1699
|
}
|
|
1638
1700
|
if (!this.domBaselines.register(baselineId, socket, result.expiresAt)) {
|
|
1639
|
-
await this.releaseOrphanDomBaseline(socket, responseMessage);
|
|
1701
|
+
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1640
1702
|
pending.lastErrorResponse = createFailure(
|
|
1641
1703
|
responseMessage.id,
|
|
1642
1704
|
ERROR_CODES.DOM_BASELINE_INVALIDATED,
|
|
@@ -1688,11 +1750,12 @@ export class BridgeDaemon {
|
|
|
1688
1750
|
if (baselineId) this.domBaselines.delete(baselineId);
|
|
1689
1751
|
await this.releaseOrphanDomBaseline(socket, responseMessage).catch(() => {});
|
|
1690
1752
|
}
|
|
1691
|
-
this.clearPendingRequest(responseMessage.id, pending)
|
|
1692
|
-
|
|
1753
|
+
if (this.clearPendingRequest(responseMessage.id, pending)) {
|
|
1754
|
+
this.recordRequestCompletion(responseMessage.id, false, pending);
|
|
1755
|
+
}
|
|
1693
1756
|
throw error;
|
|
1694
1757
|
}
|
|
1695
|
-
this.clearPendingRequest(responseMessage.id, pending, true);
|
|
1758
|
+
if (!this.clearPendingRequest(responseMessage.id, pending, true)) return;
|
|
1696
1759
|
this.recordRequestCompletion(responseMessage.id, true, pending);
|
|
1697
1760
|
this.pushLog({
|
|
1698
1761
|
at: new Date().toISOString(),
|
|
@@ -1719,6 +1782,7 @@ export class BridgeDaemon {
|
|
|
1719
1782
|
* @returns {void}
|
|
1720
1783
|
*/
|
|
1721
1784
|
handleSocketClose(socket) {
|
|
1785
|
+
this.clientSockets.delete(socket);
|
|
1722
1786
|
if (socket.__registrationTimeoutId) {
|
|
1723
1787
|
clearTimeout(socket.__registrationTimeoutId);
|
|
1724
1788
|
socket.__registrationTimeoutId = undefined;
|
|
@@ -1781,7 +1845,7 @@ export class BridgeDaemon {
|
|
|
1781
1845
|
* @returns {Promise<void>}
|
|
1782
1846
|
*/
|
|
1783
1847
|
async finishPendingRequestIfExhausted(requestId, pending) {
|
|
1784
|
-
if (pending.targets.size > 0 ||
|
|
1848
|
+
if (pending.targets.size > 0 || this.pendingRequests.get(requestId) !== pending) {
|
|
1785
1849
|
return;
|
|
1786
1850
|
}
|
|
1787
1851
|
|
|
@@ -1975,6 +2039,37 @@ export async function pingExistingDaemon(transport) {
|
|
|
1975
2039
|
});
|
|
1976
2040
|
}
|
|
1977
2041
|
|
|
2042
|
+
/**
|
|
2043
|
+
* Only a refused or missing endpoint proves a Unix socket is stale.
|
|
2044
|
+
* @param {BridgeTransport} transport
|
|
2045
|
+
* @returns {Promise<boolean>}
|
|
2046
|
+
*/
|
|
2047
|
+
export function hasLiveListener(transport) {
|
|
2048
|
+
return new Promise((resolve, reject) => {
|
|
2049
|
+
const socket =
|
|
2050
|
+
transport.type === 'tcp'
|
|
2051
|
+
? net.createConnection({ host: transport.host, port: transport.port })
|
|
2052
|
+
: net.createConnection(transport.socketPath);
|
|
2053
|
+
const timer = setTimeout(() => finish(true), DAEMON_EXISTING_SOCKET_PING_TIMEOUT_MS);
|
|
2054
|
+
/** @param {boolean} live @param {Error} [error] */
|
|
2055
|
+
function finish(live, error) {
|
|
2056
|
+
clearTimeout(timer);
|
|
2057
|
+
socket.destroy();
|
|
2058
|
+
if (error) reject(error);
|
|
2059
|
+
else resolve(live);
|
|
2060
|
+
}
|
|
2061
|
+
socket.once('connect', () => finish(true));
|
|
2062
|
+
socket.once('error', (error) => {
|
|
2063
|
+
if (
|
|
2064
|
+
'code' in error &&
|
|
2065
|
+
(error.code === 'ENOENT' || error.code === 'ECONNREFUSED' || error.code === 'ENOTSOCK')
|
|
2066
|
+
)
|
|
2067
|
+
finish(false);
|
|
2068
|
+
else finish(true, error);
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1978
2073
|
/**
|
|
1979
2074
|
* @param {Record<string, unknown>} params
|
|
1980
2075
|
* @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
|