@twinsearth/w2m-dsh-plugin 2.2.5 → 2.2.6
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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/codex/mcp-servers.toml +1 -1
- package/docs/acceptance/2.2.6/RELEASE-2260.json +30 -0
- package/lib/client.js +60 -60
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/src/plugin/tools.mjs +1 -1
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "The w2m fleet plugins: one account, many machines, any of Windows, macOS or Linux.",
|
|
8
|
-
"version": "2.2.
|
|
8
|
+
"version": "2.2.6"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "w2m-fleet",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Read fleet state and dispatch commands across the machines registered to one account. Read-only MCP surface: the five exposed tools cannot change anything.",
|
|
15
|
-
"version": "2.2.
|
|
15
|
+
"version": "2.2.6",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "TwinsEarth"
|
|
18
18
|
},
|
|
@@ -26,5 +26,5 @@
|
|
|
26
26
|
]
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
|
-
"version": "2.2.
|
|
29
|
+
"version": "2.2.6"
|
|
30
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w2m-fleet",
|
|
3
3
|
"description": "Drive the same project on online Windows, macOS and Linux machines from one account: read fleet state, dispatch commands and read results. The MCP surface is deliberately read-only.",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TwinsEarth"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.2.6] - 2026-11-14
|
|
8
|
+
|
|
9
|
+
- Fixes 搜索本对话: the walk covered only the viewport and stopped counting at six matches, so a
|
|
10
|
+
word that was merely scrolled out of sight reported `0/0` and the feature looked broken. It now
|
|
11
|
+
searches the whole rendered conversation, counts every match, and wraps up to 120 of them (the cap
|
|
12
|
+
bounds paint cost, not the count). The count shows `…` while a query is still being applied.
|
|
13
|
+
- The search settles 1s after the last keystroke instead of 3s (the 3s delay was the second half of
|
|
14
|
+
the same complaint: typing appeared to do nothing). Enter searches immediately.
|
|
15
|
+
- Marks persist after the query settles, so the scroll-triggered repaint is gone; it was re-walking
|
|
16
|
+
the tree on every scroll for a result that no longer needed refreshing.
|
|
17
|
+
- Tests: the client-bundle suite now asserts the 1s settle, the whole-conversation walk (no
|
|
18
|
+
viewport rect filter) and the count-every-match cap, instead of the old 3s/six-mark contract.
|
|
19
|
+
|
|
7
20
|
## [2.2.5] - 2026-11-12
|
|
8
21
|
|
|
9
22
|
- DSH 0.2.2-rc.2 compatibility, measured rather than assumed: the packed artifact mounts and boots in
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ w2m-mcp --help
|
|
|
93
93
|
# 复制进你自己的 codex/mcp-servers.toml
|
|
94
94
|
[mcp_servers.w2m]
|
|
95
95
|
command = "npx"
|
|
96
|
-
args = ["-y", "@twinsearth/w2m-dsh-plugin@2.2.
|
|
96
|
+
args = ["-y", "@twinsearth/w2m-dsh-plugin@2.2.6", "w2m-mcp"]
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
### 5. 验证过的 release tarball(离线)
|
package/codex/mcp-servers.toml
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.2.6",
|
|
3
|
+
"case": "RELEASE-2260",
|
|
4
|
+
"kind": "positive",
|
|
5
|
+
"machines": ["b53a37f4-1464-4512-9259-33a7df091e3e"],
|
|
6
|
+
"executed_at": "2026-11-13T19:13:00Z",
|
|
7
|
+
"executor": "lead",
|
|
8
|
+
"task_id": "pre-release-2260",
|
|
9
|
+
"requirement": "v2.2.6: the in-conversation search (search conversation, the header magnifier) counts matches across the whole rendered conversation instead of only the viewport, and starts searching 1s after the last keystroke instead of 3s",
|
|
10
|
+
"commands": [
|
|
11
|
+
["node", "--check", "lib/client.js"],
|
|
12
|
+
["node", "--test", "--test-force-exit", "test/client-bundle.test.mjs"],
|
|
13
|
+
["node", "--test", "--test-force-exit", "test/loader-entry.test.mjs"],
|
|
14
|
+
["node", "scripts/pack.mjs"]
|
|
15
|
+
],
|
|
16
|
+
"result": {
|
|
17
|
+
"outcome": "ok",
|
|
18
|
+
"assertions": {
|
|
19
|
+
"defect": "the walk was restricted to the current viewport and stopped counting at six matches, so a word that existed only above the fold reported 0/0 and the feature looked broken; the 3s settle made every query look ignored for its first three seconds",
|
|
20
|
+
"scope": "paintSearchMarks walks every text node under the conversation region (falling back to document.body), counts every occurrence, and wraps at most 120 of them -- the cap bounds paint cost, not the count; counting stops at 2000 matches so a one-letter query cannot stall the UI",
|
|
21
|
+
"settle": "the query settles 1000ms after the last keystroke (was 3000ms); Enter searches immediately; the count shows an ellipsis while the query is still being applied",
|
|
22
|
+
"no_viewport_filter": "the viewport rectangle filter and its six-match ceiling are gone, and the scroll-triggered repaint is removed because marks persist once painted",
|
|
23
|
+
"syntax": "node --check lib/client.js: exit 0",
|
|
24
|
+
"client_bundle": "test/client-bundle.test.mjs 36/36, including the rewritten contract assertions: 1s settle string, no viewportRect, count is ranges.length, no scroll repaint",
|
|
25
|
+
"loader": "test/loader-entry.test.mjs 10/10 against the packed 2.2.6 artifact in the real DSH 0.2.2-rc.3 desktop runtime (entry active, tools registered, client half composed and activated, page booted in a browser)",
|
|
26
|
+
"installed": "the packed 2.2.6 tarball is installed in this machine's desktop profile from ~/.dsh/w2m/w2m-dsh-plugin-2.2.6.tgz, so the running app serves the fixed client bundle on its next page load",
|
|
27
|
+
"artifact": "scripts/pack.mjs packs dist/twinsearth-w2m-dsh-plugin-2.2.6.tgz deterministically from this tree; the published sha256 is the one release.yml writes into dist/SHA256SUMS beside the tarball, so the record does not restate the hash of the archive that contains it"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -1109,12 +1109,22 @@ const PLUGIN_UPDATE_ROUTE = '/api/w2m/plugin-update';
|
|
|
1109
1109
|
* v1.9.11: in-conversation full-text search with visible yellow highlights.
|
|
1110
1110
|
*
|
|
1111
1111
|
* The previous version only called window.find, which the desktop shell does not reliably
|
|
1112
|
-
* surface. This walks the rendered conversation text and registers the matches through
|
|
1113
|
-
*
|
|
1114
|
-
* focused match. Matches stay yellow until the query changes or the box closes.
|
|
1112
|
+
* surface. This walks the rendered conversation text and registers the matches through local
|
|
1113
|
+
* `mark` elements (no CSS Custom Highlight dependency, so a match is always visible), then
|
|
1114
|
+
* scrolls to the focused match. Matches stay yellow until the query changes or the box closes.
|
|
1115
|
+
*
|
|
1116
|
+
* v2.2.6: the walk covers the whole rendered conversation, not just the viewport. The old
|
|
1117
|
+
* viewport-only rule reported `0` for text that was merely scrolled out of sight, and capped the
|
|
1118
|
+
* count at six, which made a matching conversation look unsearchable. The cap now only bounds how
|
|
1119
|
+
* many nodes are wrapped (paint cost), never how many matches are counted.
|
|
1115
1120
|
*/
|
|
1116
1121
|
function searchRoots() {
|
|
1117
1122
|
const roots = [];
|
|
1123
|
+
if (typeof document === 'undefined' || !document.body) return roots;
|
|
1124
|
+
const region = typeof document.querySelector === 'function'
|
|
1125
|
+
? (document.querySelector('[data-conversation-region]') ?? document.querySelector('[data-conversation-session]'))
|
|
1126
|
+
: null;
|
|
1127
|
+
const start = region ?? document.body;
|
|
1118
1128
|
const seen = new Set();
|
|
1119
1129
|
const visit = (node) => {
|
|
1120
1130
|
if (!node || seen.has(node)) return;
|
|
@@ -1125,7 +1135,8 @@ function searchRoots() {
|
|
|
1125
1135
|
for (let i = 0; i < kids.length; i += 1) visit(kids[i]);
|
|
1126
1136
|
}
|
|
1127
1137
|
};
|
|
1128
|
-
|
|
1138
|
+
roots.push(start);
|
|
1139
|
+
visit(start);
|
|
1129
1140
|
return roots;
|
|
1130
1141
|
}
|
|
1131
1142
|
function clearSearchMarks() {
|
|
@@ -1139,28 +1150,25 @@ function clearSearchMarks() {
|
|
|
1139
1150
|
}
|
|
1140
1151
|
}
|
|
1141
1152
|
/**
|
|
1142
|
-
* Search rules (v2.0.5 UI): a pure number up to 100 is not a useful query in a
|
|
1143
|
-
* ids/ports/timestamps, so it is refused outright (no marks, no scroll, no
|
|
1144
|
-
*
|
|
1153
|
+
* Search rules (v2.0.5 UI, revised in v2.2.6): a pure number up to 100 is not a useful query in a
|
|
1154
|
+
* conversation full of ids/ports/timestamps, so it is refused outright (no marks, no scroll, no
|
|
1155
|
+
* count). Every rendered match is counted; only the first {@link SEARCH_MAX_MARKS} are wrapped, so a
|
|
1156
|
+
* long conversation stays responsive without making a match outside the viewport invisible.
|
|
1145
1157
|
*/
|
|
1146
|
-
const SEARCH_MAX_MARKS =
|
|
1158
|
+
const SEARCH_MAX_MARKS = 120;
|
|
1159
|
+
/** Hard ceiling on counted matches, so a one-letter query in a huge conversation cannot stall the UI. */
|
|
1160
|
+
const SEARCH_MAX_COUNT = 2000;
|
|
1147
1161
|
function searchQueryAllowed(query) {
|
|
1148
1162
|
const text = String(query ?? '').trim();
|
|
1149
1163
|
if (text === '') return false;
|
|
1150
1164
|
if (/^\d+$/.test(text)) return Number(text) > 100;
|
|
1151
1165
|
return true;
|
|
1152
1166
|
}
|
|
1153
|
-
function viewportRect() {
|
|
1154
|
-
const width = typeof window !== 'undefined' ? window.innerWidth : 1024;
|
|
1155
|
-
const height = typeof window !== 'undefined' ? window.innerHeight : 768;
|
|
1156
|
-
return { left: 0, top: 0, right: width, bottom: height };
|
|
1157
|
-
}
|
|
1158
1167
|
function paintSearchMarks(query) {
|
|
1159
1168
|
clearSearchMarks();
|
|
1160
1169
|
if (typeof document === 'undefined' || query === '' || !searchQueryAllowed(query)) return 0;
|
|
1161
1170
|
const needle = query.toLowerCase();
|
|
1162
|
-
const
|
|
1163
|
-
let total = 0;
|
|
1171
|
+
const ranges = [];
|
|
1164
1172
|
outer: for (const root of searchRoots()) {
|
|
1165
1173
|
let walker;
|
|
1166
1174
|
try {
|
|
@@ -1176,37 +1184,37 @@ function paintSearchMarks(query) {
|
|
|
1176
1184
|
},
|
|
1177
1185
|
});
|
|
1178
1186
|
} catch { continue; }
|
|
1179
|
-
const nodes = [];
|
|
1180
1187
|
let node = walker.nextNode();
|
|
1181
|
-
while (node !== null) {
|
|
1182
|
-
|
|
1183
|
-
// Cheap pre-filter: a text node whose element is entirely outside the viewport cannot paint.
|
|
1184
|
-
const host = textNode.parentElement;
|
|
1185
|
-
const hostRect = host !== null && typeof host.getBoundingClientRect === 'function' ? host.getBoundingClientRect() : null;
|
|
1186
|
-
if (hostRect !== null && (hostRect.bottom < view.top || hostRect.top > view.bottom)) continue;
|
|
1187
|
-
const text = textNode.nodeValue ?? '';
|
|
1188
|
+
while (node !== null) {
|
|
1189
|
+
const text = node.nodeValue ?? '';
|
|
1188
1190
|
const lower = text.toLowerCase();
|
|
1189
|
-
const hits = [];
|
|
1190
1191
|
let index = lower.indexOf(needle);
|
|
1191
|
-
while (index >= 0) {
|
|
1192
|
-
for (const hit of hits) {
|
|
1192
|
+
while (index >= 0) {
|
|
1193
1193
|
try {
|
|
1194
1194
|
const range = document.createRange();
|
|
1195
|
-
range.setStart(
|
|
1196
|
-
range.setEnd(
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
mark.setAttribute('style', 'background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)');
|
|
1202
|
-
range.surroundContents(mark);
|
|
1203
|
-
total += 1;
|
|
1204
|
-
if (total >= SEARCH_MAX_MARKS) break outer;
|
|
1205
|
-
} catch { /* a range that crosses elements cannot be wrapped; skip it */ }
|
|
1195
|
+
range.setStart(node, index);
|
|
1196
|
+
range.setEnd(node, index + query.length);
|
|
1197
|
+
ranges.push(range);
|
|
1198
|
+
} catch { /* an unselectable node cannot carry a match */ }
|
|
1199
|
+
if (ranges.length >= SEARCH_MAX_COUNT) break outer;
|
|
1200
|
+
index = lower.indexOf(needle, index + query.length);
|
|
1206
1201
|
}
|
|
1202
|
+
node = walker.nextNode();
|
|
1207
1203
|
}
|
|
1208
1204
|
}
|
|
1209
|
-
|
|
1205
|
+
// Wrap from the end so the offsets of the matches still ahead of us stay valid: wrapping one range
|
|
1206
|
+
// splits its text node, and a split above an earlier range cannot move that range.
|
|
1207
|
+
const wrapCount = Math.min(ranges.length, SEARCH_MAX_MARKS);
|
|
1208
|
+
for (let i = wrapCount - 1; i >= 0; i -= 1) {
|
|
1209
|
+
const range = ranges[i];
|
|
1210
|
+
try {
|
|
1211
|
+
const mark = document.createElement('mark');
|
|
1212
|
+
mark.className = 'w2mSearchMark';
|
|
1213
|
+
mark.setAttribute('style', 'background-color:rgba(255,235,59,.85);color:rgba(11,122,51,1)');
|
|
1214
|
+
range.surroundContents(mark);
|
|
1215
|
+
} catch { /* a range that crosses elements cannot be wrapped; it stays counted */ }
|
|
1216
|
+
}
|
|
1217
|
+
return ranges.length;
|
|
1210
1218
|
}
|
|
1211
1219
|
/**
|
|
1212
1220
|
* Probe-scroll: assign a tiny scrollTop and see whether the browser keeps it.
|
|
@@ -1440,25 +1448,27 @@ function ConversationSearch() {
|
|
|
1440
1448
|
const [query, setQuery] = React.useState('');
|
|
1441
1449
|
const [count, setCount] = React.useState(0);
|
|
1442
1450
|
const [cursor, setCursor] = React.useState(-1);
|
|
1451
|
+
const [pending, setPending] = React.useState(false);
|
|
1443
1452
|
const inputRef = React.useRef(null);
|
|
1444
1453
|
React.useEffect(() => { if (open && inputRef.current && inputRef.current.focus) inputRef.current.focus(); }, [open]);
|
|
1445
|
-
// v2.
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1454
|
+
// v2.2.6: wait 1s after the last keystroke before walking the conversation (the v2.0.5 delay was
|
|
1455
|
+
// 3s, which read as "search does nothing" and was the reported complaint). One pass per settled
|
|
1456
|
+
// query is still what keeps typing cheap; the walk now covers the whole conversation and counts
|
|
1457
|
+
// every match, so a hit outside the current viewport is no longer reported as zero.
|
|
1449
1458
|
const appliedRef = React.useRef('');
|
|
1450
1459
|
const paintRef = React.useRef(() => {});
|
|
1451
1460
|
paintRef.current = (focus = false) => {
|
|
1452
1461
|
const total = paintSearchMarks(appliedRef.current);
|
|
1453
1462
|
setCount(total);
|
|
1454
1463
|
setCursor(total > 0 ? 0 : -1);
|
|
1464
|
+
setPending(false);
|
|
1455
1465
|
if (focus === true) {
|
|
1456
1466
|
const first = document.querySelector('mark.w2mSearchMark');
|
|
1457
1467
|
if (first && first.scrollIntoView) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1458
1468
|
}
|
|
1459
1469
|
};
|
|
1460
1470
|
React.useEffect(() => {
|
|
1461
|
-
if (!open) { appliedRef.current = ''; clearSearchMarks(); setCount(0); setCursor(-1); }
|
|
1471
|
+
if (!open) { appliedRef.current = ''; clearSearchMarks(); setCount(0); setCursor(-1); setPending(false); }
|
|
1462
1472
|
}, [open]);
|
|
1463
1473
|
React.useEffect(() => {
|
|
1464
1474
|
if (!open) return undefined;
|
|
@@ -1468,25 +1478,13 @@ function ConversationSearch() {
|
|
|
1468
1478
|
clearSearchMarks();
|
|
1469
1479
|
setCount(0);
|
|
1470
1480
|
setCursor(-1);
|
|
1481
|
+
setPending(false);
|
|
1471
1482
|
return undefined;
|
|
1472
1483
|
}
|
|
1473
|
-
|
|
1484
|
+
setPending(true);
|
|
1485
|
+
const timer = setTimeout(() => { appliedRef.current = query; paintRef.current(true); }, 1000);
|
|
1474
1486
|
return () => clearTimeout(timer);
|
|
1475
1487
|
}, [open, query]);
|
|
1476
|
-
React.useEffect(() => {
|
|
1477
|
-
if (!open) return undefined;
|
|
1478
|
-
let timer = null;
|
|
1479
|
-
const onScroll = () => {
|
|
1480
|
-
if (appliedRef.current === '') return;
|
|
1481
|
-
if (timer !== null) clearTimeout(timer);
|
|
1482
|
-
timer = setTimeout(() => { paintRef.current(false); }, 150);
|
|
1483
|
-
};
|
|
1484
|
-
window.addEventListener('scroll', onScroll, true);
|
|
1485
|
-
return () => {
|
|
1486
|
-
if (timer !== null) clearTimeout(timer);
|
|
1487
|
-
window.removeEventListener('scroll', onScroll, true);
|
|
1488
|
-
};
|
|
1489
|
-
}, [open]);
|
|
1490
1488
|
const step = (backwards) => {
|
|
1491
1489
|
if (appliedRef.current === '') return;
|
|
1492
1490
|
let marks = [...document.querySelectorAll('mark.w2mSearchMark')];
|
|
@@ -1519,12 +1517,14 @@ function ConversationSearch() {
|
|
|
1519
1517
|
onKeyDown: (event) => {
|
|
1520
1518
|
if (event.key === 'Enter') {
|
|
1521
1519
|
event.preventDefault();
|
|
1522
|
-
if (searchQueryAllowed(query)) { appliedRef.current = query; paintRef.current(true);
|
|
1520
|
+
if (searchQueryAllowed(query)) { appliedRef.current = query; paintRef.current(true); }
|
|
1523
1521
|
}
|
|
1524
1522
|
if (event.key === 'Escape') { setOpen(false); setQuery(''); }
|
|
1525
1523
|
},
|
|
1526
1524
|
}),
|
|
1527
|
-
h('span', { key: 'count', className: 'w2mSearchCount' },
|
|
1525
|
+
h('span', { key: 'count', className: 'w2mSearchCount' }, pending
|
|
1526
|
+
? '…'
|
|
1527
|
+
: (count === 0 ? (searchQueryAllowed(query) ? '0' : '') : whenCount(count, cursor))),
|
|
1528
1528
|
h('button', { key: 'prev', type: 'button', className: 'w2mSearchBtn', title: '上一个', onClick: () => step(true) }, '↑'),
|
|
1529
1529
|
h('button', { key: 'next', type: 'button', className: 'w2mSearchBtn', title: '下一个', onClick: () => step(false) }, '↓'),
|
|
1530
1530
|
h('button', { key: 'close', type: 'button', className: 'w2mSearchBtn', title: '关闭搜索', onClick: () => { setOpen(false); setQuery(''); clearSearchMarks(); } }, '×'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/w2m-dsh-plugin",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 11 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "twinsearth-w2m-dsh",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.6",
|
|
5
5
|
"description": "Run one project across your own Windows and macOS machines from a single DeepSeek account, through 12 w2m_* tools. Four coordination modes (replicate, split, broadcast, pipeline) and six aggregate verdicts (consistent, divergent, divergent-platform, failed, partial, unverifiable). P2P hole punching by default, with a bundled relay, STUN responder and ledger fallback, so the fleet needs no third-party service. Joining needs a pairing code plus autoStartAgent: true; installing alone does not join.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"keywords": [
|
package/src/plugin/tools.mjs
CHANGED
|
@@ -296,7 +296,7 @@ const MAX_STAGES = 16;
|
|
|
296
296
|
* `package.json` during `scripts/pack.mjs` rather than hand-maintained, and `W2M_PLUGIN_VERSION`
|
|
297
297
|
* overrides it at runtime for tests and packagers.
|
|
298
298
|
*/
|
|
299
|
-
const PLUGIN_VERSION = '2.2.
|
|
299
|
+
const PLUGIN_VERSION = '2.2.6';
|
|
300
300
|
|
|
301
301
|
/** How long any single HTTP call may take before it is aborted, in milliseconds. */
|
|
302
302
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|