mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
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
This commit is contained in:
+75
@@ -0,0 +1,75 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user