Complete TypeScript frontend migration #22
+166
-154
@@ -1,167 +1,179 @@
|
||||
"use strict";
|
||||
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr", "wefax"];
|
||||
function decodeCborUint(view, bytes, state, additional) {
|
||||
const offset = state.offset;
|
||||
if (additional < 24) return additional;
|
||||
if (additional === 24) {
|
||||
if (offset + 1 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 1;
|
||||
return bytes[offset];
|
||||
}
|
||||
if (additional === 25) {
|
||||
if (offset + 2 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 2;
|
||||
return view.getUint16(offset);
|
||||
}
|
||||
if (additional === 26) {
|
||||
if (offset + 4 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 4;
|
||||
return view.getUint32(offset);
|
||||
}
|
||||
if (additional === 27) {
|
||||
if (offset + 8 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getBigUint64(offset);
|
||||
state.offset += 8;
|
||||
const numeric = Number(value);
|
||||
if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range");
|
||||
return numeric;
|
||||
}
|
||||
throw new Error("Unsupported CBOR additional info");
|
||||
}
|
||||
function decodeCborFloat16(bits) {
|
||||
const sign = bits & 32768 ? -1 : 1;
|
||||
const exponent = bits >> 10 & 31;
|
||||
const fraction = bits & 1023;
|
||||
if (exponent === 0) {
|
||||
return fraction === 0 ? sign * 0 : sign * Math.pow(2, -14) * (fraction / 1024);
|
||||
}
|
||||
if (exponent === 31) {
|
||||
return fraction === 0 ? sign * Infinity : Number.NaN;
|
||||
}
|
||||
return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
|
||||
}
|
||||
function decodeCborItem(view, bytes, state) {
|
||||
if (state.offset >= bytes.length) throw new Error("CBOR payload truncated");
|
||||
const initial = bytes[state.offset++];
|
||||
const major = initial >> 5;
|
||||
const additional = initial & 31;
|
||||
if (major === 0) return decodeCborUint(view, bytes, state, additional);
|
||||
if (major === 1) return -1 - decodeCborUint(view, bytes, state, additional);
|
||||
if (major === 2) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const chunk = bytes.slice(state.offset, state.offset + length);
|
||||
state.offset += length;
|
||||
return Array.from(chunk);
|
||||
}
|
||||
if (major === 3) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const chunk = bytes.subarray(state.offset, state.offset + length);
|
||||
state.offset += length;
|
||||
return textDecoder ? textDecoder.decode(chunk) : String.fromCharCode(...chunk);
|
||||
}
|
||||
if (major === 4) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const items = new Array(length);
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
items[i] = decodeCborItem(view, bytes, state);
|
||||
(() => {
|
||||
// src/decode-history-worker.ts
|
||||
var textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||
var HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr", "wefax"];
|
||||
var workerScope = self;
|
||||
function decodeCborUint(view, bytes, state, additional) {
|
||||
const offset = state.offset;
|
||||
if (additional < 24) return additional;
|
||||
if (additional === 24) {
|
||||
if (offset + 1 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 1;
|
||||
return bytes[offset] ?? 0;
|
||||
}
|
||||
return items;
|
||||
if (additional === 25) {
|
||||
if (offset + 2 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 2;
|
||||
return view.getUint16(offset);
|
||||
}
|
||||
if (additional === 26) {
|
||||
if (offset + 4 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 4;
|
||||
return view.getUint32(offset);
|
||||
}
|
||||
if (additional === 27) {
|
||||
if (offset + 8 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getBigUint64(offset);
|
||||
state.offset += 8;
|
||||
const numeric = Number(value);
|
||||
if (!Number.isSafeInteger(numeric)) throw new Error("CBOR integer exceeds JS safe range");
|
||||
return numeric;
|
||||
}
|
||||
throw new Error("Unsupported CBOR additional info");
|
||||
}
|
||||
if (major === 5) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const value = {};
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
const key = decodeCborItem(view, bytes, state);
|
||||
value[String(key)] = decodeCborItem(view, bytes, state);
|
||||
function decodeCborFloat16(bits) {
|
||||
const sign = bits & 32768 ? -1 : 1;
|
||||
const exponent = bits >> 10 & 31;
|
||||
const fraction = bits & 1023;
|
||||
if (exponent === 0) {
|
||||
return fraction === 0 ? sign * 0 : sign * Math.pow(2, -14) * (fraction / 1024);
|
||||
}
|
||||
if (exponent === 31) {
|
||||
return fraction === 0 ? sign * Infinity : Number.NaN;
|
||||
}
|
||||
return sign * Math.pow(2, exponent - 15) * (1 + fraction / 1024);
|
||||
}
|
||||
function decodeCborItem(view, bytes, state) {
|
||||
if (state.offset >= bytes.length) throw new Error("CBOR payload truncated");
|
||||
const initial = bytes[state.offset++];
|
||||
if (initial === void 0) throw new Error("CBOR payload truncated");
|
||||
const major = initial >> 5;
|
||||
const additional = initial & 31;
|
||||
if (major === 0) return decodeCborUint(view, bytes, state, additional);
|
||||
if (major === 1) return -1 - decodeCborUint(view, bytes, state, additional);
|
||||
if (major === 2) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const chunk = bytes.slice(state.offset, state.offset + length);
|
||||
state.offset += length;
|
||||
return Array.from(chunk);
|
||||
}
|
||||
if (major === 3) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
if (state.offset + length > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const chunk = bytes.subarray(state.offset, state.offset + length);
|
||||
state.offset += length;
|
||||
return textDecoder ? textDecoder.decode(chunk) : String.fromCharCode(...chunk);
|
||||
}
|
||||
if (major === 4) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const items = new Array(length);
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
items[i] = decodeCborItem(view, bytes, state);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
if (major === 5) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const value = {};
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
const key = decodeCborItem(view, bytes, state);
|
||||
const property = typeof key === "string" || typeof key === "number" || typeof key === "boolean" ? String(key) : JSON.stringify(key);
|
||||
value[property] = decodeCborItem(view, bytes, state);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (major === 6) {
|
||||
decodeCborUint(view, bytes, state, additional);
|
||||
return decodeCborItem(view, bytes, state);
|
||||
}
|
||||
if (major === 7) {
|
||||
if (additional === 20) return false;
|
||||
if (additional === 21) return true;
|
||||
if (additional === 22) return null;
|
||||
if (additional === 23) return void 0;
|
||||
if (additional === 25) {
|
||||
if (state.offset + 2 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const bits = view.getUint16(state.offset);
|
||||
state.offset += 2;
|
||||
return decodeCborFloat16(bits);
|
||||
}
|
||||
if (additional === 26) {
|
||||
if (state.offset + 4 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getFloat32(state.offset);
|
||||
state.offset += 4;
|
||||
return value;
|
||||
}
|
||||
if (additional === 27) {
|
||||
if (state.offset + 8 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getFloat64(state.offset);
|
||||
state.offset += 8;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
throw new Error("Unsupported CBOR major type");
|
||||
}
|
||||
function decodeCborPayload(buffer) {
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const state = { offset: 0 };
|
||||
const value = decodeCborItem(view, bytes, state);
|
||||
if (state.offset !== bytes.length) {
|
||||
throw new Error("Unexpected trailing bytes in decode history payload");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (major === 6) {
|
||||
decodeCborUint(view, bytes, state, additional);
|
||||
return decodeCborItem(view, bytes, state);
|
||||
function isHistory(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
if (major === 7) {
|
||||
if (additional === 20) return false;
|
||||
if (additional === 21) return true;
|
||||
if (additional === 22) return null;
|
||||
if (additional === 23) return void 0;
|
||||
if (additional === 25) {
|
||||
if (state.offset + 2 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const bits = view.getUint16(state.offset);
|
||||
state.offset += 2;
|
||||
return decodeCborFloat16(bits);
|
||||
async function fetchAndDecodeHistory(url, batchLimit) {
|
||||
workerScope.postMessage({ type: "status", phase: "fetching" });
|
||||
const resp = await fetch(url, { credentials: "same-origin" });
|
||||
if (!resp.ok) throw new Error(`History fetch failed: ${String(resp.status)}`);
|
||||
const payload = await resp.arrayBuffer();
|
||||
if (payload.byteLength === 0) {
|
||||
workerScope.postMessage({ type: "start", total: 0 });
|
||||
workerScope.postMessage({ type: "done", total: 0 });
|
||||
return;
|
||||
}
|
||||
if (additional === 26) {
|
||||
if (state.offset + 4 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getFloat32(state.offset);
|
||||
state.offset += 4;
|
||||
return value;
|
||||
}
|
||||
if (additional === 27) {
|
||||
if (state.offset + 8 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
const value = view.getFloat64(state.offset);
|
||||
state.offset += 8;
|
||||
return value;
|
||||
workerScope.postMessage({ type: "status", phase: "decoding" });
|
||||
const history = decodeCborPayload(payload);
|
||||
const total = HISTORY_GROUP_KEYS.reduce((sum, key) => {
|
||||
const items = isHistory(history) && Array.isArray(history[key]) ? history[key] : [];
|
||||
return sum + items.length;
|
||||
}, 0);
|
||||
workerScope.postMessage({ type: "start", total });
|
||||
let processed = 0;
|
||||
const safeLimit = Math.max(1, Math.min(2048, Number(batchLimit) || 512));
|
||||
for (const kind of HISTORY_GROUP_KEYS) {
|
||||
const items = isHistory(history) && Array.isArray(history[kind]) ? history[kind] : [];
|
||||
if (items.length === 0) continue;
|
||||
for (let index = 0; index < items.length; index += safeLimit) {
|
||||
const messages = items.slice(index, index + safeLimit);
|
||||
processed += messages.length;
|
||||
workerScope.postMessage({
|
||||
type: "group",
|
||||
kind,
|
||||
messages,
|
||||
processed,
|
||||
total
|
||||
});
|
||||
}
|
||||
}
|
||||
workerScope.postMessage({ type: "done", total });
|
||||
}
|
||||
throw new Error("Unsupported CBOR major type");
|
||||
}
|
||||
function decodeCborPayload(buffer) {
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const state = { offset: 0 };
|
||||
const value = decodeCborItem(view, bytes, state);
|
||||
if (state.offset !== bytes.length) {
|
||||
throw new Error("Unexpected trailing bytes in decode history payload");
|
||||
function isFetchHistoryRequest(value) {
|
||||
return typeof value === "object" && value !== null && "type" in value && value.type === "fetch-history";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
async function fetchAndDecodeHistory(url, batchLimit) {
|
||||
self.postMessage({ type: "status", phase: "fetching" });
|
||||
const resp = await fetch(url, { credentials: "same-origin" });
|
||||
if (!resp.ok) throw new Error(`History fetch failed: ${resp.status}`);
|
||||
const payload = await resp.arrayBuffer();
|
||||
if (!payload || payload.byteLength === 0) {
|
||||
self.postMessage({ type: "start", total: 0 });
|
||||
self.postMessage({ type: "done", total: 0 });
|
||||
return;
|
||||
}
|
||||
self.postMessage({ type: "status", phase: "decoding" });
|
||||
const history = decodeCborPayload(payload);
|
||||
const total = HISTORY_GROUP_KEYS.reduce((sum, key) => {
|
||||
const items = history && Array.isArray(history[key]) ? history[key] : [];
|
||||
return sum + items.length;
|
||||
}, 0);
|
||||
self.postMessage({ type: "start", total });
|
||||
let processed = 0;
|
||||
const safeLimit = Math.max(1, Math.min(2048, Number(batchLimit) || 512));
|
||||
for (const kind of HISTORY_GROUP_KEYS) {
|
||||
const items = history && Array.isArray(history[kind]) ? history[kind] : [];
|
||||
if (items.length === 0) continue;
|
||||
for (let index = 0; index < items.length; index += safeLimit) {
|
||||
const messages = items.slice(index, index + safeLimit);
|
||||
processed += messages.length;
|
||||
self.postMessage({
|
||||
type: "group",
|
||||
kind,
|
||||
messages,
|
||||
processed,
|
||||
total
|
||||
workerScope.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
if (!isFetchHistoryRequest(data)) return;
|
||||
fetchAndDecodeHistory(data.url || "/decode/history", data.batchLimit).catch((error) => {
|
||||
workerScope.postMessage({
|
||||
type: "error",
|
||||
message: error instanceof Error ? error.message : typeof error === "string" ? error : "unknown worker failure"
|
||||
});
|
||||
}
|
||||
}
|
||||
self.postMessage({ type: "done", total });
|
||||
}
|
||||
self.onmessage = (event) => {
|
||||
const data = event?.data || {};
|
||||
if (data?.type !== "fetch-history") return;
|
||||
fetchAndDecodeHistory(data.url || "/decode/history", data.batchLimit).catch((err) => {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
message: err && err.message ? err.message : String(err || "unknown worker failure")
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -21,7 +21,6 @@ await build({
|
||||
"map-core": path.join(sourceDir, "map-core.js"),
|
||||
screenshot: path.join(sourceDir, "screenshot.js"),
|
||||
"webgl-renderer": path.join(sourceDir, "webgl-renderer.js"),
|
||||
"decode-history-worker": path.join(sourceDir, "decode-history-worker.js"),
|
||||
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.js"),
|
||||
ais: path.join(sourceDir, "plugins", "ais.js"),
|
||||
aprs: path.join(sourceDir, "plugins", "aprs.js"),
|
||||
@@ -48,3 +47,16 @@ await build({
|
||||
charset: "utf8",
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
await build({
|
||||
entryPoints: [path.join(sourceDir, "decode-history-worker.ts")],
|
||||
outfile: path.join(outputDir, "decode-history-worker.js"),
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
sourcemap: false,
|
||||
legalComments: "inline",
|
||||
charset: "utf8",
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
+52
-28
@@ -2,16 +2,27 @@
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr", "wefax"];
|
||||
export {};
|
||||
|
||||
function decodeCborUint(view, bytes, state, additional) {
|
||||
const textDecoder = typeof TextDecoder === "function" ? new TextDecoder() : null;
|
||||
const HISTORY_GROUP_KEYS = ["ais", "vdes", "aprs", "hf_aprs", "cw", "ft8", "ft4", "ft2", "wspr", "wefax"] as const;
|
||||
type HistoryGroup = (typeof HISTORY_GROUP_KEYS)[number];
|
||||
type CborValue = number | string | boolean | null | undefined | CborValue[] | { [key: string]: CborValue };
|
||||
interface DecodeState { offset: number }
|
||||
interface FetchHistoryRequest { type: "fetch-history"; url?: string; batchLimit?: number }
|
||||
interface WorkerScope {
|
||||
postMessage(message: unknown): void;
|
||||
onmessage: ((event: MessageEvent<unknown>) => void) | null;
|
||||
}
|
||||
const workerScope = self as unknown as WorkerScope;
|
||||
|
||||
function decodeCborUint(view: DataView, bytes: Uint8Array, state: DecodeState, additional: number): number {
|
||||
const offset = state.offset;
|
||||
if (additional < 24) return additional;
|
||||
if (additional === 24) {
|
||||
if (offset + 1 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
state.offset += 1;
|
||||
return bytes[offset];
|
||||
return bytes[offset] ?? 0;
|
||||
}
|
||||
if (additional === 25) {
|
||||
if (offset + 2 > bytes.length) throw new Error("CBOR payload truncated");
|
||||
@@ -34,7 +45,7 @@ function decodeCborUint(view, bytes, state, additional) {
|
||||
throw new Error("Unsupported CBOR additional info");
|
||||
}
|
||||
|
||||
function decodeCborFloat16(bits) {
|
||||
function decodeCborFloat16(bits: number): number {
|
||||
const sign = (bits & 0x8000) ? -1 : 1;
|
||||
const exponent = (bits >> 10) & 0x1f;
|
||||
const fraction = bits & 0x03ff;
|
||||
@@ -47,9 +58,10 @@ function decodeCborFloat16(bits) {
|
||||
return sign * Math.pow(2, exponent - 15) * (1 + (fraction / 1024));
|
||||
}
|
||||
|
||||
function decodeCborItem(view, bytes, state) {
|
||||
function decodeCborItem(view: DataView, bytes: Uint8Array, state: DecodeState): CborValue {
|
||||
if (state.offset >= bytes.length) throw new Error("CBOR payload truncated");
|
||||
const initial = bytes[state.offset++];
|
||||
if (initial === undefined) throw new Error("CBOR payload truncated");
|
||||
const major = initial >> 5;
|
||||
const additional = initial & 0x1f;
|
||||
if (major === 0) return decodeCborUint(view, bytes, state, additional);
|
||||
@@ -70,7 +82,7 @@ function decodeCborItem(view, bytes, state) {
|
||||
}
|
||||
if (major === 4) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const items = new Array(length);
|
||||
const items: CborValue[] = new Array<CborValue>(length);
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
items[i] = decodeCborItem(view, bytes, state);
|
||||
}
|
||||
@@ -78,10 +90,13 @@ function decodeCborItem(view, bytes, state) {
|
||||
}
|
||||
if (major === 5) {
|
||||
const length = decodeCborUint(view, bytes, state, additional);
|
||||
const value = {};
|
||||
const value: Record<string, CborValue> = {};
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
const key = decodeCborItem(view, bytes, state);
|
||||
value[String(key)] = decodeCborItem(view, bytes, state);
|
||||
const property = typeof key === "string" || typeof key === "number" || typeof key === "boolean"
|
||||
? String(key)
|
||||
: JSON.stringify(key);
|
||||
value[property] = decodeCborItem(view, bytes, state);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -116,7 +131,7 @@ function decodeCborItem(view, bytes, state) {
|
||||
throw new Error("Unsupported CBOR major type");
|
||||
}
|
||||
|
||||
function decodeCborPayload(buffer) {
|
||||
function decodeCborPayload(buffer: ArrayBuffer | Uint8Array): CborValue {
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const state = { offset: 0 };
|
||||
@@ -127,35 +142,39 @@ function decodeCborPayload(buffer) {
|
||||
return value;
|
||||
}
|
||||
|
||||
async function fetchAndDecodeHistory(url, batchLimit) {
|
||||
self.postMessage({ type: "status", phase: "fetching" });
|
||||
function isHistory(value: CborValue): value is Partial<Record<HistoryGroup, CborValue[]>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function fetchAndDecodeHistory(url: string, batchLimit?: number) {
|
||||
workerScope.postMessage({ type: "status", phase: "fetching" });
|
||||
const resp = await fetch(url, { credentials: "same-origin" });
|
||||
if (!resp.ok) throw new Error(`History fetch failed: ${resp.status}`);
|
||||
if (!resp.ok) throw new Error(`History fetch failed: ${String(resp.status)}`);
|
||||
const payload = await resp.arrayBuffer();
|
||||
if (!payload || payload.byteLength === 0) {
|
||||
self.postMessage({ type: "start", total: 0 });
|
||||
self.postMessage({ type: "done", total: 0 });
|
||||
if (payload.byteLength === 0) {
|
||||
workerScope.postMessage({ type: "start", total: 0 });
|
||||
workerScope.postMessage({ type: "done", total: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
self.postMessage({ type: "status", phase: "decoding" });
|
||||
workerScope.postMessage({ type: "status", phase: "decoding" });
|
||||
const history = decodeCborPayload(payload);
|
||||
const total = HISTORY_GROUP_KEYS.reduce((sum, key) => {
|
||||
const items = history && Array.isArray(history[key]) ? history[key] : [];
|
||||
const items = isHistory(history) && Array.isArray(history[key]) ? history[key] : [];
|
||||
return sum + items.length;
|
||||
}, 0);
|
||||
self.postMessage({ type: "start", total });
|
||||
workerScope.postMessage({ type: "start", total });
|
||||
|
||||
let processed = 0;
|
||||
const safeLimit = Math.max(1, Math.min(2048, Number(batchLimit) || 512));
|
||||
|
||||
for (const kind of HISTORY_GROUP_KEYS) {
|
||||
const items = history && Array.isArray(history[kind]) ? history[kind] : [];
|
||||
const items = isHistory(history) && Array.isArray(history[kind]) ? history[kind] : [];
|
||||
if (items.length === 0) continue;
|
||||
for (let index = 0; index < items.length; index += safeLimit) {
|
||||
const messages = items.slice(index, index + safeLimit);
|
||||
processed += messages.length;
|
||||
self.postMessage({
|
||||
workerScope.postMessage({
|
||||
type: "group",
|
||||
kind,
|
||||
messages,
|
||||
@@ -164,17 +183,22 @@ async function fetchAndDecodeHistory(url, batchLimit) {
|
||||
});
|
||||
}
|
||||
}
|
||||
self.postMessage({ type: "done", total });
|
||||
workerScope.postMessage({ type: "done", total });
|
||||
}
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const data = event?.data || {};
|
||||
if (data?.type !== "fetch-history") return;
|
||||
function isFetchHistoryRequest(value: unknown): value is FetchHistoryRequest {
|
||||
return typeof value === "object" && value !== null && "type" in value && value.type === "fetch-history";
|
||||
}
|
||||
|
||||
workerScope.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
if (!isFetchHistoryRequest(data)) return;
|
||||
fetchAndDecodeHistory(data.url || "/decode/history", data.batchLimit)
|
||||
.catch((err) => {
|
||||
self.postMessage({
|
||||
.catch((error: unknown) => {
|
||||
workerScope.postMessage({
|
||||
type: "error",
|
||||
message: err && err.message ? err.message : String(err || "unknown worker failure"),
|
||||
message: error instanceof Error ? error.message
|
||||
: typeof error === "string" ? error : "unknown worker failure",
|
||||
});
|
||||
});
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import vm from "node:vm";
|
||||
|
||||
test("decode-history worker decodes CBOR and batches messages", async () => {
|
||||
const messages = [];
|
||||
const worker = {
|
||||
onmessage: null,
|
||||
postMessage(message) { messages.push(message); },
|
||||
};
|
||||
const payload = Uint8Array.from([0xa1, 0x63, 0x61, 0x69, 0x73, 0x83, 0x01, 0x02, 0x03]);
|
||||
const context = vm.createContext({
|
||||
self: worker,
|
||||
TextDecoder,
|
||||
Uint8Array,
|
||||
DataView,
|
||||
ArrayBuffer,
|
||||
Number,
|
||||
Math,
|
||||
String,
|
||||
Error,
|
||||
fetch: async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
arrayBuffer: async () => payload.buffer,
|
||||
}),
|
||||
});
|
||||
const source = await readFile(
|
||||
new URL("../../assets/web/generated/decode-history-worker.js", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
new vm.Script(source).runInContext(context);
|
||||
worker.onmessage({ data: { type: "fetch-history", url: "/decode/history", batchLimit: 2 } });
|
||||
await new Promise((resolve) => { setTimeout(resolve, 0); });
|
||||
|
||||
assert.deepEqual(
|
||||
messages.map(({ type, phase, total, processed }) => ({ type, phase, total, processed })),
|
||||
[
|
||||
{ type: "status", phase: "fetching", total: undefined, processed: undefined },
|
||||
{ type: "status", phase: "decoding", total: undefined, processed: undefined },
|
||||
{ type: "start", phase: undefined, total: 3, processed: undefined },
|
||||
{ type: "group", phase: undefined, total: 3, processed: 2 },
|
||||
{ type: "group", phase: undefined, total: 3, processed: 3 },
|
||||
{ type: "done", phase: undefined, total: 3, processed: undefined },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(Array.from(messages[3].messages), [1, 2]);
|
||||
assert.deepEqual(Array.from(messages[4].messages), [3]);
|
||||
});
|
||||
Reference in New Issue
Block a user