Skip to content

Commit e79353b

Browse files
committed
feat(code.gs): Implement batch processing to avoid GAS fetchAll limits
Split requests into batches of 50 items to prevent hitting Google Apps Script fetchAll method limitations. - Add batching logic using Map structure to avoid lists index syncronization overhead - Refactor _doBatch core logic for batch processing - Add helper function for batch handling
1 parent 48317c7 commit e79353b

1 file changed

Lines changed: 94 additions & 45 deletions

File tree

assets/apps_script/Code.gs

Lines changed: 94 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,57 @@ function _doSingle (req) {
253253
}
254254
}
255255

256+
/**
257+
* Helper to process a batch of 50 requests
258+
* @param { Array<GoogleAppsScript.URL_Fetch.URLFetchRequest>} fetchBatch
259+
* @param { Array<number>} batchIndices
260+
* @param { Map<number, RequestMapData>} requestMap
261+
* @param { Map<number, Object> } errorMap
262+
*/
263+
function _processBatch (fetchBatch, batchIndices, requestMap, errorMap) {
264+
try {
265+
const responses = UrlFetchApp.fetchAll(fetchBatch);
266+
for (let j = 0; j < responses.length; j++) {
267+
const originalIndex = batchIndices[ j ];
268+
requestMap.get(originalIndex).response = responses[ j ];
269+
}
270+
} catch (fetchAllErr) {
271+
const fetchAllErrMsg = String(fetchAllErr);
272+
Logger.log(`[WARN] fetchAll failed, retrying: ${ fetchAllErrMsg }`);
273+
274+
// Fallback: retry safe methods individually
275+
for (let j = 0; j < fetchBatch.length; j++) {
276+
const originalIndex = batchIndices[ j ];
277+
const reqObj = requestMap.get(originalIndex);
278+
279+
if (!SAFE_REPLAY_METHODS.has(reqObj.method)) {
280+
errorMap.set(originalIndex, "batch fetchAll failed; unsafe method not replayed");
281+
continue;
282+
}
283+
284+
try {
285+
const resp = UrlFetchApp.fetch(reqObj.request.url, reqObj.request);
286+
reqObj.response = resp;
287+
} catch (singleErr) {
288+
const singleErrMsg = String(singleErr);
289+
Logger.log(`[ERROR] _doBatch fallback[${ originalIndex }]: ${ singleErrMsg }`);
290+
errorMap.set(originalIndex, singleErrMsg);
291+
}
292+
}
293+
}
294+
}
295+
256296
/**
257297
* @param {Array<ClientRequest>} items
258298
* @returns {GoogleAppsScript.Content.TextOutput}
259299
*/
260300
function _doBatch (items) {
261-
let fetchItems = [];
262-
let fetchIndices = [];
263-
let fetchMethods = [];
301+
/** @type {Map<number, {request: Object, method: string, response: GoogleAppsScript.URL_Fetch.HTTPResponse | null}>} */
302+
let requestMap = new Map();
303+
/** @type {Map<number, string>} */
264304
let errorMap = new Map();
265305

306+
// Build request map
266307
for (let i = 0; i < items.length; i++) {
267308
let item = items[ i ];
268309
if (!item || typeof item !== "object") {
@@ -276,70 +317,78 @@ function _doBatch (items) {
276317
continue;
277318
}
278319
try {
279-
fetchItems.push({ url: item.u, ..._buildOpts(item) });
280-
fetchIndices.push(i);
281-
fetchMethods.push((item.m?.toLowerCase?.() ?? "get"));
320+
const method = (item.m?.toLowerCase?.() ?? "get");
321+
const request = { url: item.u, ..._buildOpts(item) };
322+
requestMap.set(i, { request, method, response: null });
282323
} catch (buildErr) {
283324
const errMsg = String(buildErr);
284325
Logger.log(`[ERROR] _doBatch[${ i }] _buildOpts: ${ errMsg }`);
285326
errorMap.set(i, errMsg);
286327
}
287328
}
288329

289-
// fetchAll() processes all requests in parallel inside Google. If it
290-
// throws as a whole (e.g. one URL violates UrlFetchApp limits and
291-
// poisons the whole batch), degrade to per-item fetch on safe methods
292-
// so a single bad request does not zero out every response in the
293-
// batch. Mirrors upstream `masterking32/MasterHttpRelayVPN@3094288`.
294-
let responses = [];
295-
try {
296-
// Single - chunk fast path; avoids the fetchAll overhead for the common case.
297-
if (fetchItems.length === 1) {
298-
responses = [ UrlFetchApp.fetch(fetchItems[ 0 ].url, fetchItems[ 0 ]) ];
299-
} else {
300-
responses = UrlFetchApp.fetchAll(fetchItems);
330+
if (requestMap.size === 0) {
331+
// All items failed validation
332+
let results = [];
333+
for (let i = 0; i < items.length; i++) {
334+
results.push({ e: String(errorMap.get(i)) });
301335
}
302-
} catch (fetchAllErr) {
303-
const fetchAllErrMsg = String(fetchAllErr);
304-
responses = [];
305-
for (let j = 0; j < fetchItems.length; j++) {
306-
try {
307-
if (!SAFE_REPLAY_METHODS.has(fetchMethods[ j ])) {
308-
errorMap.set(fetchIndices[ j ], "batch fetchAll failed; unsafe method not replayed");
309-
responses[ j ] = null;
310-
continue;
311-
}
312-
let fallbackReq = fetchItems[ j ];
313-
responses[ j ] = UrlFetchApp.fetch(fallbackReq.url, fallbackReq);
314-
} catch (singleErr) {
315-
const singleErrMsg = String(singleErr);
316-
Logger.log(`[ERROR] _doBatch (fetching single item): ${ fetchIndices[ j ] }`);
317-
errorMap.set(fetchIndices[ j ], singleErrMsg);
318-
responses[ j ] = null;
336+
return _relayResponse({ q: results });
337+
}
338+
339+
// Single-item fast path
340+
if (requestMap.size === 1) {
341+
const [ originalIndex, data ] = requestMap.entries().next().value;
342+
try {
343+
const resp = UrlFetchApp.fetch(data.request.url, data.request);
344+
data.response = resp;
345+
} catch (singleErr) {
346+
const singleErrMsg = String(singleErr);
347+
Logger.log(`[ERROR] _doBatch (fetching single item): ${ singleErrMsg }`);
348+
errorMap.set(originalIndex, singleErrMsg);
349+
}
350+
351+
} else {
352+
// Batch mode
353+
let requestCount = 0;
354+
let fetchBatch = [];
355+
let batchIndices = [];
356+
357+
for (const [ originalIndex, data ] of requestMap) {
358+
fetchBatch.push(data.request);
359+
batchIndices.push(originalIndex);
360+
requestCount++;
361+
362+
// Process batch when it reaches 50 or we're at the end
363+
if (fetchBatch.length === 50 || requestCount === requestMap.size) {
364+
_processBatch(fetchBatch, batchIndices, requestMap, errorMap);
365+
fetchBatch = [];
366+
batchIndices = [];
319367
}
320368
}
321369
}
322370

371+
// Build results
372+
/** @type {Array<RelaySingleResponse | RelayErrorResponse>} */
323373
let results = [];
324374
for (let i = 0; i < items.length; i++) {
325375
if (errorMap.has(i)) {
326376
results.push({ e: String(errorMap.get(i)) });
327-
} else {
328-
const fetchPos = fetchIndices.indexOf(i);
329-
if (fetchPos === -1 || !responses[ fetchPos ]) {
330-
results.push({ e: "fetch failed" });
331-
} else {
332-
try {
333-
results.push(_buildResponse(responses[ fetchPos ]));
334-
} catch (err) {
335-
results.push({ e: `fetch failed: ${ String(err) }` });
336-
}
377+
} else if (requestMap.has(i) && requestMap.get(i).response) {
378+
try {
379+
results.push(_buildResponse(requestMap.get(i).response));
380+
} catch (err) {
381+
results.push({ e: `fetch failed: ${ String(err) }` });
337382
}
383+
} else {
384+
results.push({ e: "fetch failed" });
338385
}
339386
}
387+
340388
return _relayResponse({ q: results });
341389
}
342390

391+
/** @param {GoogleAppsScript.Events.DoPost} e */
343392
function doPost (e) {
344393
if (!AUTH_KEY) {
345394
Logger.log("[ERROR] doPost: AUTH_KEY not configured");

0 commit comments

Comments
 (0)