@geometra/mcp 1.49.0 → 1.53.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/__tests__/ats-integration.test.js +1 -0
- package/dist/__tests__/connect-utils.test.js +82 -3
- package/dist/__tests__/proxy-session-actions.test.js +66 -0
- package/dist/__tests__/server-batch-results.test.js +273 -1
- package/dist/__tests__/server-session-resolution.test.d.ts +1 -0
- package/dist/__tests__/server-session-resolution.test.js +88 -0
- package/dist/connect-utils.d.ts +2 -0
- package/dist/connect-utils.js +5 -3
- package/dist/server.js +295 -7
- package/dist/session.d.ts +6 -0
- package/dist/session.js +158 -25
- package/package.json +1 -1
package/dist/session.js
CHANGED
|
@@ -22,6 +22,7 @@ const FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS = 500;
|
|
|
22
22
|
const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 225;
|
|
23
23
|
const FILL_BATCH_FILE_FIELD_TIMEOUT_MS = 5000;
|
|
24
24
|
const FILL_BATCH_MAX_TIMEOUT_MS = 60_000;
|
|
25
|
+
const SESSION_RECONNECT_TIMEOUT_MS = 5_000;
|
|
25
26
|
let nextRequestSequence = 0;
|
|
26
27
|
function invalidateSessionCaches(session) {
|
|
27
28
|
session.cachedA11y = null;
|
|
@@ -682,6 +683,8 @@ export function connect(url, opts) {
|
|
|
682
683
|
heartbeatInterval.unref();
|
|
683
684
|
}
|
|
684
685
|
ws.on('pong', () => {
|
|
686
|
+
if (session.ws !== ws)
|
|
687
|
+
return;
|
|
685
688
|
lastMessageAt = Date.now();
|
|
686
689
|
pendingPongBy = null;
|
|
687
690
|
});
|
|
@@ -693,6 +696,8 @@ export function connect(url, opts) {
|
|
|
693
696
|
}
|
|
694
697
|
}, 10_000);
|
|
695
698
|
ws.on('open', () => {
|
|
699
|
+
if (session.ws !== ws)
|
|
700
|
+
return;
|
|
696
701
|
if (session.connectTrace) {
|
|
697
702
|
session.connectTrace.wsOpenMs = performance.now() - startedAt;
|
|
698
703
|
}
|
|
@@ -715,6 +720,8 @@ export function connect(url, opts) {
|
|
|
715
720
|
}
|
|
716
721
|
});
|
|
717
722
|
ws.on('message', (data) => {
|
|
723
|
+
if (session.ws !== ws)
|
|
724
|
+
return;
|
|
718
725
|
lastMessageAt = Date.now();
|
|
719
726
|
try {
|
|
720
727
|
const msg = JSON.parse(String(data));
|
|
@@ -755,6 +762,8 @@ export function connect(url, opts) {
|
|
|
755
762
|
}
|
|
756
763
|
});
|
|
757
764
|
ws.on('close', () => {
|
|
765
|
+
if (session.ws !== ws)
|
|
766
|
+
return;
|
|
758
767
|
if (heartbeatInterval) {
|
|
759
768
|
clearInterval(heartbeatInterval);
|
|
760
769
|
heartbeatInterval = null;
|
|
@@ -846,6 +855,19 @@ export function getSession(id) {
|
|
|
846
855
|
return activeSessions.get(defaultSessionId) ?? null;
|
|
847
856
|
return null;
|
|
848
857
|
}
|
|
858
|
+
export function pruneDisconnectedSessions() {
|
|
859
|
+
const removedIds = [];
|
|
860
|
+
for (const [id, session] of activeSessions.entries()) {
|
|
861
|
+
if (session.ws.readyState === WebSocket.OPEN)
|
|
862
|
+
continue;
|
|
863
|
+
removedIds.push(id);
|
|
864
|
+
activeSessions.delete(id);
|
|
865
|
+
if (defaultSessionId === id) {
|
|
866
|
+
promoteDefaultSession();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return removedIds;
|
|
870
|
+
}
|
|
849
871
|
export function resolveSession(id) {
|
|
850
872
|
if (id) {
|
|
851
873
|
const found = activeSessions.get(id);
|
|
@@ -903,6 +925,9 @@ function estimateFillBatchTimeout(fields) {
|
|
|
903
925
|
totalTextLength += field.value.length;
|
|
904
926
|
total += FILL_BATCH_TEXT_FIELD_TIMEOUT_MS;
|
|
905
927
|
total += Math.ceil(Math.max(1, field.value.length) / FILL_BATCH_TEXT_LENGTH_SLICE) * FILL_BATCH_TEXT_LENGTH_TIMEOUT_MS;
|
|
928
|
+
if (field.typingDelayMs !== undefined) {
|
|
929
|
+
total += field.typingDelayMs * field.value.length;
|
|
930
|
+
}
|
|
906
931
|
break;
|
|
907
932
|
case 'choice':
|
|
908
933
|
total += field.choiceType === 'group' ? FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS : FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
|
|
@@ -956,16 +981,128 @@ export function waitForUiCondition(session, predicate, timeoutMs) {
|
|
|
956
981
|
check();
|
|
957
982
|
});
|
|
958
983
|
}
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
984
|
+
function reconnectUrlForSession(session) {
|
|
985
|
+
if (session.proxyRuntime && typeof session.proxyRuntime.wsUrl === 'string') {
|
|
986
|
+
return session.proxyRuntime.wsUrl;
|
|
987
|
+
}
|
|
988
|
+
const pooled = reusableProxyEntryForSession(session);
|
|
989
|
+
if (pooled) {
|
|
990
|
+
return pooled.wsUrl;
|
|
991
|
+
}
|
|
992
|
+
if (typeof session.url === 'string' && /^wss?:\/\//i.test(session.url)) {
|
|
993
|
+
return session.url;
|
|
994
|
+
}
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
async function openWebSocket(url, timeoutMs = SESSION_RECONNECT_TIMEOUT_MS) {
|
|
998
|
+
return await new Promise((resolve, reject) => {
|
|
999
|
+
const ws = new WebSocket(url);
|
|
1000
|
+
const timeout = setTimeout(() => {
|
|
1001
|
+
cleanup();
|
|
1002
|
+
try {
|
|
1003
|
+
ws.close();
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
/* ignore */
|
|
1007
|
+
}
|
|
1008
|
+
reject(new Error(`Reconnect to ${url} timed out after ${timeoutMs}ms`));
|
|
1009
|
+
}, timeoutMs);
|
|
1010
|
+
const onOpen = () => {
|
|
1011
|
+
cleanup();
|
|
1012
|
+
resolve(ws);
|
|
1013
|
+
};
|
|
1014
|
+
const onError = (err) => {
|
|
1015
|
+
cleanup();
|
|
1016
|
+
reject(new Error(`WebSocket reconnect failed for ${url}: ${err.message}`));
|
|
1017
|
+
};
|
|
1018
|
+
function cleanup() {
|
|
1019
|
+
clearTimeout(timeout);
|
|
1020
|
+
ws.off('open', onOpen);
|
|
1021
|
+
ws.off('error', onError);
|
|
1022
|
+
}
|
|
1023
|
+
ws.on('open', onOpen);
|
|
1024
|
+
ws.on('error', onError);
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function bindReconnectedSocket(session, ws) {
|
|
1028
|
+
ws.on('message', data => {
|
|
1029
|
+
if (session.ws !== ws)
|
|
963
1030
|
return;
|
|
1031
|
+
try {
|
|
1032
|
+
const msg = JSON.parse(String(data));
|
|
1033
|
+
if (msg.type === 'frame') {
|
|
1034
|
+
session.layout = msg.layout;
|
|
1035
|
+
session.tree = msg.tree;
|
|
1036
|
+
session.updateRevision++;
|
|
1037
|
+
invalidateSessionCaches(session);
|
|
1038
|
+
}
|
|
1039
|
+
else if (msg.type === 'patch' && session.layout) {
|
|
1040
|
+
applyPatches(session.layout, msg.patches);
|
|
1041
|
+
session.updateRevision++;
|
|
1042
|
+
invalidateSessionCaches(session);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
/* ignore malformed messages */
|
|
964
1047
|
}
|
|
965
|
-
const startRevision = session.updateRevision;
|
|
966
|
-
session.ws.send(JSON.stringify({ type: 'resize', width, height }));
|
|
967
|
-
waitForNextUpdate(session, timeoutMs, undefined, startRevision).then(resolve).catch(reject);
|
|
968
1048
|
});
|
|
1049
|
+
ws.on('close', () => {
|
|
1050
|
+
if (session.ws !== ws)
|
|
1051
|
+
return;
|
|
1052
|
+
if (activeSessions.get(session.id) === session) {
|
|
1053
|
+
activeSessions.delete(session.id);
|
|
1054
|
+
if (defaultSessionId === session.id)
|
|
1055
|
+
promoteDefaultSession();
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
async function ensureSessionConnected(session) {
|
|
1060
|
+
if (session.ws.readyState === WebSocket.OPEN)
|
|
1061
|
+
return;
|
|
1062
|
+
if (session.reconnectInFlight) {
|
|
1063
|
+
const recovered = await session.reconnectInFlight;
|
|
1064
|
+
if (!recovered) {
|
|
1065
|
+
throw new Error('Not connected');
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const targetUrl = reconnectUrlForSession(session);
|
|
1070
|
+
if (!targetUrl) {
|
|
1071
|
+
throw new Error('Not connected');
|
|
1072
|
+
}
|
|
1073
|
+
const reconnectPromise = (async () => {
|
|
1074
|
+
const nextWs = await openWebSocket(targetUrl);
|
|
1075
|
+
try {
|
|
1076
|
+
session.ws.close();
|
|
1077
|
+
}
|
|
1078
|
+
catch {
|
|
1079
|
+
/* ignore */
|
|
1080
|
+
}
|
|
1081
|
+
session.ws = nextWs;
|
|
1082
|
+
bindReconnectedSocket(session, nextWs);
|
|
1083
|
+
activeSessions.set(session.id, session);
|
|
1084
|
+
if (!session.isolated) {
|
|
1085
|
+
defaultSessionId = session.id;
|
|
1086
|
+
}
|
|
1087
|
+
return true;
|
|
1088
|
+
})();
|
|
1089
|
+
session.reconnectInFlight = reconnectPromise;
|
|
1090
|
+
let recovered = false;
|
|
1091
|
+
try {
|
|
1092
|
+
recovered = await reconnectPromise;
|
|
1093
|
+
}
|
|
1094
|
+
finally {
|
|
1095
|
+
session.reconnectInFlight = undefined;
|
|
1096
|
+
}
|
|
1097
|
+
if (!recovered) {
|
|
1098
|
+
throw new Error('Not connected');
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async function sendResizeAndWaitForUpdate(session, width, height, timeoutMs = 5_000) {
|
|
1102
|
+
await ensureSessionConnected(session);
|
|
1103
|
+
const startRevision = session.updateRevision;
|
|
1104
|
+
session.ws.send(JSON.stringify({ type: 'resize', width, height }));
|
|
1105
|
+
return await waitForNextUpdate(session, timeoutMs, undefined, startRevision);
|
|
969
1106
|
}
|
|
970
1107
|
/**
|
|
971
1108
|
* Send a click event at (x, y) and wait for the next frame/patch response.
|
|
@@ -982,11 +1119,8 @@ export function sendClick(session, x, y, timeoutMs) {
|
|
|
982
1119
|
* Send a sequence of key events to type text into the focused element.
|
|
983
1120
|
*/
|
|
984
1121
|
export function sendType(session, text, timeoutMs) {
|
|
985
|
-
return
|
|
986
|
-
|
|
987
|
-
reject(new Error('Not connected'));
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
1122
|
+
return (async () => {
|
|
1123
|
+
await ensureSessionConnected(session);
|
|
990
1124
|
// Send each character as keydown + keyup
|
|
991
1125
|
for (const char of text) {
|
|
992
1126
|
const keyEvent = {
|
|
@@ -1003,8 +1137,8 @@ export function sendType(session, text, timeoutMs) {
|
|
|
1003
1137
|
session.ws.send(JSON.stringify({ ...keyEvent, eventType: 'onKeyUp' }));
|
|
1004
1138
|
}
|
|
1005
1139
|
// Wait briefly for server to process and send update
|
|
1006
|
-
waitForNextUpdate(session, timeoutMs)
|
|
1007
|
-
});
|
|
1140
|
+
return await waitForNextUpdate(session, timeoutMs);
|
|
1141
|
+
})();
|
|
1008
1142
|
}
|
|
1009
1143
|
/**
|
|
1010
1144
|
* Send a special key (Enter, Tab, Escape, etc.)
|
|
@@ -1054,6 +1188,10 @@ export function sendFieldText(session, fieldLabel, value, opts, timeoutMs) {
|
|
|
1054
1188
|
payload.exact = opts.exact;
|
|
1055
1189
|
if (opts?.fieldId)
|
|
1056
1190
|
payload.fieldId = opts.fieldId;
|
|
1191
|
+
if (opts?.typingDelayMs !== undefined)
|
|
1192
|
+
payload.typingDelayMs = opts.typingDelayMs;
|
|
1193
|
+
if (opts?.imeFriendly !== undefined)
|
|
1194
|
+
payload.imeFriendly = opts.imeFriendly;
|
|
1057
1195
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
1058
1196
|
}
|
|
1059
1197
|
/** Choose a value for a labeled choice field (select, custom combobox, or radio-style group). */
|
|
@@ -2955,17 +3093,12 @@ function applyPatches(layout, patches) {
|
|
|
2955
3093
|
node.height = patch.height;
|
|
2956
3094
|
}
|
|
2957
3095
|
}
|
|
2958
|
-
function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, opts) {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
const requestId = `req-${++nextRequestSequence}`;
|
|
2965
|
-
const startRevision = session.updateRevision;
|
|
2966
|
-
session.ws.send(JSON.stringify({ ...message, requestId }));
|
|
2967
|
-
waitForNextUpdate(session, timeoutMs, requestId, startRevision, opts).then(resolve).catch(reject);
|
|
2968
|
-
});
|
|
3096
|
+
async function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, opts) {
|
|
3097
|
+
await ensureSessionConnected(session);
|
|
3098
|
+
const requestId = `req-${++nextRequestSequence}`;
|
|
3099
|
+
const startRevision = session.updateRevision;
|
|
3100
|
+
session.ws.send(JSON.stringify({ ...message, requestId }));
|
|
3101
|
+
return await waitForNextUpdate(session, timeoutMs, requestId, startRevision, opts);
|
|
2969
3102
|
}
|
|
2970
3103
|
function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision, opts) {
|
|
2971
3104
|
return new Promise((resolve, reject) => {
|
package/package.json
CHANGED