@aguacerowx/mapsgl 0.0.53 → 0.0.55

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.
@@ -19,19 +19,119 @@ import {
19
19
  import type { NexradSite } from './PreprocessedSweepParser.js';
20
20
  import { archiveCache, setArchiveCache, type DecodedRadarFrame } from './nexradArchiveCache.js';
21
21
  import { loadNexradSites } from './loadNexradSites.js';
22
+ export { setNexradSitesJsonUrl, setNexradSitesFetchAuth } from './loadNexradSites.js';
22
23
  import { sampleNexradFrameAtLatLon } from './nexradCrossSectionSampleAtLatLon.js';
24
+ import { decodeRadarSlotMessage, type DecodeSlotRequest } from './radarDecodeSlot.js';
25
+ import { nexradArchiveDiag, redactApiKeyFromUrl } from './nexradArchiveDiag.js';
23
26
 
24
27
  // seek-bzip pulls in node-bzip paths that use `new Buffer()` as a global; browsers have no Buffer.
25
28
  if (typeof (globalThis as { Buffer?: typeof Buffer }).Buffer === 'undefined') {
26
29
  (globalThis as { Buffer: typeof Buffer }).Buffer = Buffer;
27
30
  }
28
31
 
32
+ /** RN/Hermes has no global {@link DOMException}; keep AbortError semantics for fetch abort paths. */
33
+ function createAbortError(message = 'Aborted'): Error {
34
+ const g = globalThis as typeof globalThis & { DOMException?: typeof DOMException };
35
+ if (typeof g.DOMException !== 'undefined') {
36
+ return new g.DOMException(message, 'AbortError');
37
+ }
38
+ const err = new Error(message);
39
+ err.name = 'AbortError';
40
+ return err;
41
+ }
42
+
43
+ function isAbortError(err: unknown): boolean {
44
+ if (err == null || typeof err !== 'object') return false;
45
+ return (err as Error).name === 'AbortError';
46
+ }
47
+
29
48
  let NEXRAD_ARCHIVE_API_KEY = '';
49
+ /** Same as {@link AguaceroCore} grid fetches: `x-app-identifier` on React Native when set. */
50
+ let NEXRAD_ARCHIVE_BUNDLE_ID = '';
51
+
30
52
  export function setNexradArchiveApiKey(k: string) {
31
53
  NEXRAD_ARCHIVE_API_KEY = k || '';
32
54
  }
33
55
 
56
+ export function setNexradArchiveBundleId(bundleId: string) {
57
+ NEXRAD_ARCHIVE_BUNDLE_ID = bundleId || '';
58
+ }
59
+
60
+ /** Match {@link AguaceroCore} `urlWithApiKeyParam` for the same CloudFront distribution. */
61
+ function cloudFrontUrlWithApiKeyQuery(baseUrl: string): string {
62
+ if (!NEXRAD_ARCHIVE_API_KEY) return baseUrl;
63
+ const sep = baseUrl.includes('?') ? '&' : '?';
64
+ return `${baseUrl}${sep}apiKey=${NEXRAD_ARCHIVE_API_KEY}`;
65
+ }
66
+
67
+ /** Level-II: match AguaceroCore grid `fetch` headers (x-api-key, x-app-identifier on RN when bundleId set) plus Range. */
68
+ function level2CloudFrontFetchHeaders(range: string | undefined): Record<string, string> {
69
+ const headers: Record<string, string> = {
70
+ 'x-api-key': NEXRAD_ARCHIVE_API_KEY,
71
+ };
72
+ if (range !== undefined) {
73
+ headers['Range'] = range;
74
+ }
75
+ const g = globalThis as typeof globalThis & { navigator?: { product?: string } };
76
+ if (g.navigator?.product === 'ReactNative' && NEXRAD_ARCHIVE_BUNDLE_ID) {
77
+ headers['x-app-identifier'] = NEXRAD_ARCHIVE_BUNDLE_ID;
78
+ }
79
+ return headers;
80
+ }
81
+
82
+ type RadarDecodeWorkerResponse = {
83
+ type: 'DECODE_RESULT';
84
+ requestId: number;
85
+ gateData: Uint8Array | null;
86
+ nRays?: number;
87
+ nGates?: number;
88
+ stationLat?: number;
89
+ stationLon?: number;
90
+ firstGateKm?: number;
91
+ gateWidthKm?: number;
92
+ valueScale?: number;
93
+ valueOffset?: number;
94
+ rayBoundariesDeg?: Float32Array;
95
+ error?: string;
96
+ };
97
+
98
+ function shouldDecodeRadarOnMainThread(): boolean {
99
+ const g = globalThis as typeof globalThis & { navigator?: { product?: string } };
100
+ return typeof Worker === 'undefined' || g.navigator?.product === 'ReactNative';
101
+ }
102
+
103
+ /** Hermes/RN has no web Workers; mirror Worker.postMessage/onmessage on the JS thread. */
104
+ class MainThreadRadarDecodeWorker implements Pick<Worker, 'postMessage' | 'onmessage' | 'onerror' | 'terminate'> {
105
+ onmessage: ((ev: MessageEvent<RadarDecodeWorkerResponse>) => void) | null = null;
106
+ onerror: ((ev: ErrorEvent) => void) | null = null;
107
+
108
+ postMessage(data: unknown): void {
109
+ const msg = data as DecodeSlotRequest;
110
+ if (!msg || msg.type !== 'DECODE_SLOT') return;
111
+ queueMicrotask(() => {
112
+ try {
113
+ const response = decodeRadarSlotMessage(msg);
114
+ this.onmessage?.({ data: response } as MessageEvent<RadarDecodeWorkerResponse>);
115
+ } catch (error) {
116
+ this.onmessage?.({
117
+ data: {
118
+ type: 'DECODE_RESULT',
119
+ requestId: msg.requestId,
120
+ gateData: null,
121
+ error: error instanceof Error ? error.message : 'Main-thread radar decode failed',
122
+ },
123
+ } as MessageEvent<RadarDecodeWorkerResponse>);
124
+ }
125
+ });
126
+ }
127
+
128
+ terminate(): void {}
129
+ }
130
+
34
131
  function createRadarDecodeWorker(): Worker {
132
+ if (shouldDecodeRadarOnMainThread()) {
133
+ return new MainThreadRadarDecodeWorker() as unknown as Worker;
134
+ }
35
135
  return new Worker(new URL('./radarDecode.worker.bundled.js', import.meta.url), { type: 'module' });
36
136
  }
37
137
 
@@ -115,22 +215,6 @@ const inflightFetchMeta = new Map<
115
215
  >();
116
216
  let radarFetchRequestSeq = 0;
117
217
 
118
- type RadarDecodeWorkerResponse = {
119
- type: 'DECODE_RESULT';
120
- requestId: number;
121
- gateData: Uint8Array | null;
122
- nRays?: number;
123
- nGates?: number;
124
- stationLat?: number;
125
- stationLon?: number;
126
- firstGateKm?: number;
127
- gateWidthKm?: number;
128
- valueScale?: number;
129
- valueOffset?: number;
130
- rayBoundariesDeg?: Float32Array;
131
- error?: string;
132
- };
133
-
134
218
  let radarDecodeWorkers: Worker[] = [];
135
219
  let radarDecodeRequestId = 0;
136
220
  const radarDecodePending = new Map<number, {
@@ -1544,7 +1628,7 @@ function decodeSweepInWorker(
1544
1628
  priority: 'display' | 'prefetch',
1545
1629
  signal?: AbortSignal,
1546
1630
  ): Promise<DecodedRadarFrame | null> {
1547
- if (signal?.aborted) return Promise.reject(new DOMException('Aborted', 'AbortError'));
1631
+ if (signal?.aborted) return Promise.reject(createAbortError());
1548
1632
 
1549
1633
  const { worker } = getDecodeWorkerForPriority(priority);
1550
1634
  const requestId = ++radarDecodeRequestId;
@@ -1560,7 +1644,7 @@ function decodeSweepInWorker(
1560
1644
  radarDecodePending.delete(requestId);
1561
1645
  radarDecodeRequestMeta.delete(requestId);
1562
1646
  signal?.removeEventListener('abort', onAbort);
1563
- reject(new DOMException('Aborted', 'AbortError'));
1647
+ reject(createAbortError());
1564
1648
  };
1565
1649
 
1566
1650
  radarDecodePending.set(requestId, {
@@ -1719,15 +1803,30 @@ export async function fetchAndParseArchive(
1719
1803
  }
1720
1804
 
1721
1805
  // ── Level-2 two-request range path ───────────────────────────────────────
1806
+ const level2Url = cloudFrontUrlWithApiKeyQuery(url);
1807
+ nexradArchiveDiag('level2.pipeline.start', {
1808
+ objectKey,
1809
+ radarVariable,
1810
+ groupId,
1811
+ apiKeyLen: NEXRAD_ARCHIVE_API_KEY.length,
1812
+ bundleIdLen: NEXRAD_ARCHIVE_BUNDLE_ID.length,
1813
+ urlNoSecret: redactApiKeyFromUrl(level2Url),
1814
+ });
1722
1815
  // Request 1: fetch just the header + slot index to find byte offsets
1723
1816
  const azBlockBytes = LEVEL2_AZ_BLOCK_BYTES;
1724
1817
  const INDEX_FETCH_BYTES =
1725
1818
  FILE_HDR_BYTES + azBlockBytes + LEVEL2_FILE_NYQUIST_BYTES + MAX_SLOTS * SLOT_INDEX_ENTRY;
1726
- const indexResp = await fetch(url, {
1727
- headers: {
1728
- 'x-api-key': NEXRAD_ARCHIVE_API_KEY,
1729
- 'Range': `bytes=0-${INDEX_FETCH_BYTES - 1}`,
1730
- },
1819
+ nexradArchiveDiag('level2.index.fetch', {
1820
+ objectKey,
1821
+ byteRangeEnd: INDEX_FETCH_BYTES - 1,
1822
+ });
1823
+ const indexResp = await fetch(level2Url, {
1824
+ headers: level2CloudFrontFetchHeaders(`bytes=0-${INDEX_FETCH_BYTES - 1}`),
1825
+ });
1826
+ nexradArchiveDiag('level2.index.response', {
1827
+ objectKey,
1828
+ status: indexResp.status,
1829
+ ok: indexResp.ok,
1731
1830
  });
1732
1831
  if (!indexResp.ok && indexResp.status !== 206) {
1733
1832
  throw new Error(`HTTP ${indexResp.status} fetching level2 index ${url}`);
@@ -1793,16 +1892,24 @@ export async function fetchAndParseArchive(
1793
1892
  }
1794
1893
 
1795
1894
  const slotRangeEnd = slotEndExclusive - 1;
1796
- const slotHeaders: Record<string, string> = {
1797
- 'x-api-key': NEXRAD_ARCHIVE_API_KEY,
1798
- 'Range': `bytes=${slot.offset}-${slotRangeEnd}`,
1799
- };
1800
- let slotResp = await fetch(url, { headers: slotHeaders });
1895
+ const slotHeaders = level2CloudFrontFetchHeaders(`bytes=${slot.offset}-${slotRangeEnd}`);
1896
+ nexradArchiveDiag('level2.slot.fetch', {
1897
+ objectKey,
1898
+ slotIdx,
1899
+ compressedSize: slot.compressedSize,
1900
+ offset: slot.offset,
1901
+ });
1902
+ let slotResp = await fetch(level2Url, { headers: slotHeaders });
1903
+ nexradArchiveDiag('level2.slot.response', {
1904
+ objectKey,
1905
+ status: slotResp.status,
1906
+ ok: slotResp.ok,
1907
+ });
1801
1908
  let slotBuffer: ArrayBuffer;
1802
1909
  if (slotResp.ok || slotResp.status === 206) {
1803
1910
  slotBuffer = await slotResp.arrayBuffer();
1804
1911
  } else if (slotResp.status === 416) {
1805
- const fullResp = await fetch(url, { headers: { 'x-api-key': NEXRAD_ARCHIVE_API_KEY } });
1912
+ const fullResp = await fetch(level2Url, { headers: level2CloudFrontFetchHeaders(undefined) });
1806
1913
  if (!fullResp.ok) {
1807
1914
  throw new Error(`HTTP ${fullResp.status} full fetch after 416 for level2 ${url}`);
1808
1915
  }
@@ -1818,6 +1925,7 @@ export async function fetchAndParseArchive(
1818
1925
  }
1819
1926
 
1820
1927
  // ── Step 4: decode in worker ────────────────────────────────
1928
+ nexradArchiveDiag('level2.decode.start', { objectKey, radarVariable });
1821
1929
  const sites = await loadNexradSites();
1822
1930
  let decoded = await decodeSweepInWorker(
1823
1931
  objectKey, slotBuffer, header, sites,
@@ -1836,11 +1944,22 @@ export async function fetchAndParseArchive(
1836
1944
  );
1837
1945
  }
1838
1946
  setArchiveCache(cacheKey, decoded);
1947
+ nexradArchiveDiag('level2.decode.done', {
1948
+ objectKey,
1949
+ radarVariable,
1950
+ nRays: decoded.nRays ?? null,
1951
+ nGates: decoded.nGates ?? null,
1952
+ });
1839
1953
  return decoded;
1840
1954
  } catch (err) {
1841
- if (err instanceof DOMException && err.name === 'AbortError') {
1955
+ if (isAbortError(err)) {
1842
1956
  return null;
1843
1957
  }
1958
+ nexradArchiveDiag('fetchAndParseArchive.failed', {
1959
+ objectKey,
1960
+ radarSource,
1961
+ message: err instanceof Error ? err.message : String(err),
1962
+ });
1844
1963
  console.error(`[RadarLayer] fetchAndParseArchive failed for ${objectKey}:`, err);
1845
1964
  return null;
1846
1965
  } finally {
@@ -619,7 +619,7 @@ function decompress(dat, buf) {
619
619
  return cct(bufs, ol);
620
620
  }
621
621
 
622
- // src/nexrad/radarDecode.worker.ts
622
+ // src/nexrad/radarDecodeSlot.ts
623
623
  var SLOT_HDR_BYTES = 28;
624
624
  function isZstd(bytes) {
625
625
  return bytes.length >= 4 && bytes[0] === 40 && bytes[1] === 181 && bytes[2] === 47 && bytes[3] === 253;
@@ -657,9 +657,7 @@ function buildRayBoundariesDeg(azimuths) {
657
657
  boundaries[nRays] = azimuths[nRays - 1] + avgSpacing / 2;
658
658
  return boundaries;
659
659
  }
660
- self.onmessage = (event) => {
661
- const data = event.data;
662
- if (!data || data.type !== "DECODE_SLOT") return;
660
+ function decodeRadarSlotMessage(data) {
663
661
  const {
664
662
  requestId,
665
663
  objectKey,
@@ -671,137 +669,143 @@ self.onmessage = (event) => {
671
669
  azimuthsBuffer,
672
670
  sites
673
671
  } = data;
674
- try {
675
- const compressed = new Uint8Array(slotBuffer);
676
- if (!isZstd(compressed)) {
677
- self.postMessage({
678
- type: "DECODE_RESULT",
679
- requestId,
680
- gateData: null,
681
- error: `Slot for ${objectKey} is not valid zstd`
682
- });
683
- return;
684
- }
685
- const decompressed = decompress(compressed);
686
- const buffer = decompressed.buffer.slice(
687
- decompressed.byteOffset,
688
- decompressed.byteOffset + decompressed.byteLength
689
- );
690
- const view = new DataView(buffer);
691
- const valueScale = view.getFloat32(0, false);
692
- const valueOffset = view.getFloat32(4, false);
693
- const present = view.getUint32(8, false);
694
- if (present === 0) {
695
- self.postMessage({
696
- type: "DECODE_RESULT",
697
- requestId,
698
- gateData: null,
699
- error: `Slot for ${objectKey} marked not present`
700
- });
701
- return;
702
- }
703
- const slotNRays = view.getUint16(12, false);
704
- const slotNGates = view.getUint16(14, false);
705
- const gateBytes = buffer.byteLength - SLOT_HDR_BYTES;
706
- const expectedGates = slotNRays * slotNGates;
707
- const bytesPerGate = gateBytes / expectedGates;
708
- if (bytesPerGate !== 1 && bytesPerGate !== 2 && bytesPerGate !== 4) {
709
- self.postMessage({
710
- type: "DECODE_RESULT",
711
- requestId,
712
- gateData: null,
713
- error: `Unexpected bytesPerGate=${bytesPerGate} for ${objectKey}`
714
- });
715
- return;
716
- }
717
- const keyParts = parseObjectKey(objectKey);
718
- if (!keyParts) {
719
- self.postMessage({
720
- type: "DECODE_RESULT",
721
- requestId,
722
- gateData: null,
723
- error: `Unable to parse station from key: ${objectKey}`
724
- });
725
- return;
726
- }
727
- const site = sites.find((s) => s.id === keyParts.stationId);
728
- if (!site) {
729
- self.postMessage({
730
- type: "DECODE_RESULT",
731
- requestId,
732
- gateData: null,
733
- error: `Station "${keyParts.stationId}" not found`
734
- });
735
- return;
672
+ const compressed = new Uint8Array(slotBuffer);
673
+ if (!isZstd(compressed)) {
674
+ return {
675
+ type: "DECODE_RESULT",
676
+ requestId,
677
+ gateData: null,
678
+ error: `Slot for ${objectKey} is not valid zstd`
679
+ };
680
+ }
681
+ const decompressed = decompress(compressed);
682
+ const buffer = decompressed.buffer.slice(
683
+ decompressed.byteOffset,
684
+ decompressed.byteOffset + decompressed.byteLength
685
+ );
686
+ const view = new DataView(buffer);
687
+ const valueScale = view.getFloat32(0, false);
688
+ const valueOffset = view.getFloat32(4, false);
689
+ const present = view.getUint32(8, false);
690
+ if (present === 0) {
691
+ return {
692
+ type: "DECODE_RESULT",
693
+ requestId,
694
+ gateData: null,
695
+ error: `Slot for ${objectKey} marked not present`
696
+ };
697
+ }
698
+ const slotNRays = view.getUint16(12, false);
699
+ const slotNGates = view.getUint16(14, false);
700
+ const gateBytes = buffer.byteLength - SLOT_HDR_BYTES;
701
+ const expectedGates = slotNRays * slotNGates;
702
+ const bytesPerGate = gateBytes / expectedGates;
703
+ if (bytesPerGate !== 1 && bytesPerGate !== 2 && bytesPerGate !== 4) {
704
+ return {
705
+ type: "DECODE_RESULT",
706
+ requestId,
707
+ gateData: null,
708
+ error: `Unexpected bytesPerGate=${bytesPerGate} for ${objectKey}`
709
+ };
710
+ }
711
+ const keyParts = parseObjectKey(objectKey);
712
+ if (!keyParts) {
713
+ return {
714
+ type: "DECODE_RESULT",
715
+ requestId,
716
+ gateData: null,
717
+ error: `Unable to parse station from key: ${objectKey}`
718
+ };
719
+ }
720
+ const site = sites.find((s) => s.id === keyParts.stationId);
721
+ if (!site) {
722
+ return {
723
+ type: "DECODE_RESULT",
724
+ requestId,
725
+ gateData: null,
726
+ error: `Station "${keyParts.stationId}" not found`
727
+ };
728
+ }
729
+ const azimuths = new Float32Array(slotNRays);
730
+ const azView = new DataView(azimuthsBuffer);
731
+ const headerNRays = azimuthsBuffer.byteLength / 4;
732
+ for (let i = 0; i < slotNRays; i++) {
733
+ if (i < headerNRays) {
734
+ azimuths[i] = azView.getFloat32(i * 4, false);
735
+ } else {
736
+ const prevAz = i > 0 ? azimuths[i - 1] : 0;
737
+ azimuths[i] = (prevAz + 360 / slotNRays) % 360;
736
738
  }
737
- const azimuths = new Float32Array(slotNRays);
738
- const azView = new DataView(azimuthsBuffer);
739
- const headerNRays = azimuthsBuffer.byteLength / 4;
740
- for (let i = 0; i < slotNRays; i++) {
741
- if (i < headerNRays) {
742
- azimuths[i] = azView.getFloat32(i * 4, false);
743
- } else {
744
- const prevAz = i > 0 ? azimuths[i - 1] : 0;
745
- azimuths[i] = (prevAz + 360 / slotNRays) % 360;
739
+ }
740
+ const rayBoundariesDeg = buildRayBoundariesDeg(azimuths);
741
+ const gateCount = slotNRays * slotNGates;
742
+ const gateDataCopy = new Uint8Array(slotNRays * slotNGates * 2);
743
+ const outView = new DataView(gateDataCopy.buffer);
744
+ if (bytesPerGate === 1) {
745
+ const raw = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount);
746
+ for (let ray = 0; ray < slotNRays; ray++) {
747
+ let prev = 0;
748
+ for (let g = 0; g < slotNGates; g++) {
749
+ const idx = ray * slotNGates + g;
750
+ const delta = raw[idx];
751
+ const val = prev + delta & 255;
752
+ prev = val;
753
+ if (val === 0) {
754
+ outView.setInt16(idx * 2, -32768, false);
755
+ } else {
756
+ outView.setInt16(idx * 2, val, false);
757
+ }
746
758
  }
747
759
  }
748
- const rayBoundariesDeg = buildRayBoundariesDeg(azimuths);
749
- const gateCount = slotNRays * slotNGates;
750
- const gateDataCopy = new Uint8Array(slotNRays * slotNGates * 2);
751
- const outView = new DataView(gateDataCopy.buffer);
752
- if (bytesPerGate === 1) {
753
- const raw = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount);
754
- for (let ray = 0; ray < slotNRays; ray++) {
755
- let prev = 0;
756
- for (let g = 0; g < slotNGates; g++) {
757
- const idx = ray * slotNGates + g;
758
- const delta = raw[idx];
759
- const val = prev + delta & 255;
760
- prev = val;
761
- if (val === 0) {
762
- outView.setInt16(idx * 2, -32768, false);
763
- } else {
764
- outView.setInt16(idx * 2, val, false);
765
- }
766
- }
760
+ } else {
761
+ const rawBytes = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount * 2);
762
+ for (let ray = 0; ray < slotNRays; ray++) {
763
+ let prev = 0;
764
+ for (let g = 0; g < slotNGates; g++) {
765
+ const idx = ray * slotNGates + g;
766
+ const hi = rawBytes[idx * 2];
767
+ const lo = rawBytes[idx * 2 + 1];
768
+ const delta = (hi << 8 | lo) << 16 >> 16;
769
+ const val = prev + delta | 0;
770
+ prev = val;
771
+ outView.setInt16(idx * 2, val, false);
767
772
  }
773
+ }
774
+ }
775
+ return {
776
+ type: "DECODE_RESULT",
777
+ requestId,
778
+ gateData: gateDataCopy,
779
+ stationLat: site.lat,
780
+ stationLon: site.lon,
781
+ firstGateKm,
782
+ gateWidthKm,
783
+ valueScale,
784
+ valueOffset,
785
+ rayBoundariesDeg,
786
+ nRays: slotNRays,
787
+ nGates: slotNGates
788
+ };
789
+ }
790
+
791
+ // src/nexrad/radarDecode.worker.ts
792
+ self.onmessage = (event) => {
793
+ const data = event.data;
794
+ if (!data || data.type !== "DECODE_SLOT") return;
795
+ try {
796
+ const response = decodeRadarSlotMessage(data);
797
+ if (response.gateData && response.rayBoundariesDeg) {
798
+ self.postMessage(response, [
799
+ response.gateData.buffer,
800
+ response.rayBoundariesDeg.buffer
801
+ ]);
768
802
  } else {
769
- const rawBytes = new Uint8Array(buffer, SLOT_HDR_BYTES, gateCount * 2);
770
- for (let ray = 0; ray < slotNRays; ray++) {
771
- let prev = 0;
772
- for (let g = 0; g < slotNGates; g++) {
773
- const idx = ray * slotNGates + g;
774
- const hi = rawBytes[idx * 2];
775
- const lo = rawBytes[idx * 2 + 1];
776
- const delta = (hi << 8 | lo) << 16 >> 16;
777
- const val = prev + delta | 0;
778
- prev = val;
779
- outView.setInt16(idx * 2, val, false);
780
- }
781
- }
803
+ self.postMessage(response);
782
804
  }
783
- const response = {
784
- type: "DECODE_RESULT",
785
- requestId,
786
- gateData: gateDataCopy,
787
- stationLat: site.lat,
788
- stationLon: site.lon,
789
- firstGateKm,
790
- gateWidthKm,
791
- valueScale,
792
- valueOffset,
793
- rayBoundariesDeg,
794
- nRays: slotNRays,
795
- nGates: slotNGates
796
- };
797
- self.postMessage(response, [
798
- gateDataCopy.buffer,
799
- rayBoundariesDeg.buffer
800
- ]);
801
805
  } catch (error) {
802
806
  self.postMessage({
803
807
  type: "DECODE_RESULT",
804
- requestId,
808
+ requestId: data.requestId,
805
809
  gateData: null,
806
810
  error: error instanceof Error ? error.message : "Unknown worker error"
807
811
  });