Files
SubMiner/vendor/hachidori/extension/response-limits.js
T
sudacode d9fdc7ef6d feat(dictionary): add Hachidori backend support
- Add backend selection, setup gating, Anki integration, and external host support
- Add launcher flags, documentation, packaging, and focused tests
- Open on-demand overlay modals on the first attempt
2026-09-22 00:21:19 -07:00

76 lines
3.5 KiB
JavaScript

// SPDX-License-Identifier: GPL-3.0-or-later
const MAX_LOOKUP_RESPONSE_BYTES = 32 * 1024 * 1024;
const MAX_MEDIA_RESPONSE_BYTES = 6 * 1024 * 1024;
// Browser adaptation of the source's 1 MiB control frame, only for reader
// options messages. Dictionary state, custom source, and archives are separate.
const MAX_OPTIONS_FRAME_BYTES = 1024 * 1024;
const BOUNDED_REQUESTS = new Set([
"hd_frequencies",
"hd_lookup",
"hd_lookup_dictionary",
"hd_kanji",
"hd_media",
"hd_capture_asset",
"hd_options_write",
]);
const responseFrameEncoder = new TextEncoder();
export function isBoundedRequest(type) {
return BOUNDED_REQUESTS.has(type);
}
export function responseLimitError(type) {
if (type === "hd_options_write" || type === "hd_options_write_result") {
return "reader options message exceeds the 1 MiB serialized limit";
}
return ["hd_media", "hd_media_result", "hd_capture_asset", "hd_capture_asset_result"].includes(type)
? "media response exceeds the 6 MiB serialized limit"
: "lookup response exceeds the 32 MiB serialized limit";
}
export function validResponseRequestId(value) {
return value === null || typeof value === "string" || Number.isFinite(value);
}
export function responseFits(reply, nativeJsonLength = 0) {
if (reply.type === "hd_options_write" || reply.type === "hd_options_write_result") {
const json = JSON.stringify(reply);
// UTF-8 cannot be shorter than JSON's UTF-16 code-unit count.
if (json.length > MAX_OPTIONS_FRAME_BYTES) return false;
return json.length * 3 <= MAX_OPTIONS_FRAME_BYTES
|| responseFrameEncoder.encode(json).byteLength <= MAX_OPTIONS_FRAME_BYTES;
}
if (reply.type === "hd_media_result" || reply.type === "hd_capture_asset_result") {
// The producer constructs dataUrl only from fixed ASCII MIME strings and
// base64. Count that payload exactly without serializing/copying it again.
const field = reply.type === "hd_media_result" ? "dataUrl" : "data";
const dataLength = typeof reply[field] === "string" ? reply[field].length : 0;
const frame = JSON.stringify(dataLength ? { ...reply, [field]: "" } : reply);
return frame.length * 3 + dataLength <= MAX_MEDIA_RESPONSE_BYTES
|| responseFrameEncoder.encode(frame).byteLength + dataLength <= MAX_MEDIA_RESPONSE_BYTES;
}
// Extra empty endpoint fields only enlarge this conservative envelope; near
// the boundary we always measure the actual reply instead.
const envelope = nativeJsonLength === 0 ? reply : { ...reply, results: [], kanji: null };
const envelopeJson = JSON.stringify(envelope);
// JSON escaping needs at most six bytes per UTF-16 code unit, including
// lone surrogates. Ordinary native replies need no second full traversal.
if ((nativeJsonLength + envelopeJson.length) * 6 <= MAX_LOOKUP_RESPONSE_BYTES) return true;
const json = nativeJsonLength === 0 ? envelopeJson : JSON.stringify(reply);
return responseFrameEncoder.encode(json).byteLength <= MAX_LOOKUP_RESPONSE_BYTES;
}
export function boundResponseFailure(reply) {
if (!isBoundedRequest(reply.type.replace(/_result$/u, ""))) return reply;
if (!validResponseRequestId(reply.requestId)) reply.requestId = null;
if (!responseFits(reply)) {
const limitError = responseLimitError(reply.type);
const unchangedError = reply.error === limitError;
reply.error = limitError;
// Replacing an identical error cannot make the same oversized frame fit.
if (unchangedError || !responseFits(reply)) reply.requestId = null;
}
return reply;
}