@aguacerowx/react-native 0.0.51 → 0.0.52

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.
Files changed (41) hide show
  1. package/android/src/main/java/com/aguacerowx/reactnative/SatelliteLayerView.java +11 -3
  2. package/android/src/main/java/com/aguacerowx/reactnative/WeatherFrameProcessorModule.java +315 -311
  3. package/ios/SatelliteLayerView.swift +11 -4
  4. package/ios/WeatherFrameProcessorModule.swift +222 -219
  5. package/lib/commonjs/WeatherLayerManager.js +61 -45
  6. package/lib/commonjs/WeatherLayerManager.js.map +1 -1
  7. package/lib/commonjs/aguaceroRnDebug.js +8 -1
  8. package/lib/commonjs/aguaceroRnDebug.js.map +1 -1
  9. package/lib/commonjs/gridCdnAuth.js +64 -0
  10. package/lib/commonjs/gridCdnAuth.js.map +1 -0
  11. package/lib/commonjs/index.js +13 -0
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/nexrad/nexradAndroidController.js +25 -25
  14. package/lib/commonjs/nexrad/nexradAndroidController.js.map +1 -1
  15. package/lib/commonjs/nexrad/nexradDiag.js +24 -24
  16. package/lib/commonjs/satellite/satelliteAndroidController.js +15 -15
  17. package/lib/module/WeatherLayerManager.js +61 -45
  18. package/lib/module/WeatherLayerManager.js.map +1 -1
  19. package/lib/module/aguaceroRnDebug.js +8 -1
  20. package/lib/module/aguaceroRnDebug.js.map +1 -1
  21. package/lib/module/gridCdnAuth.js +56 -0
  22. package/lib/module/gridCdnAuth.js.map +1 -0
  23. package/lib/module/index.js +1 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/nexrad/nexradAndroidController.js +25 -25
  26. package/lib/module/nexrad/nexradAndroidController.js.map +1 -1
  27. package/lib/module/nexrad/nexradDiag.js +24 -24
  28. package/lib/module/satellite/satelliteAndroidController.js +15 -15
  29. package/lib/typescript/WeatherLayerManager.d.ts.map +1 -1
  30. package/lib/typescript/aguaceroRnDebug.d.ts.map +1 -1
  31. package/lib/typescript/gridCdnAuth.d.ts +24 -0
  32. package/lib/typescript/gridCdnAuth.d.ts.map +1 -0
  33. package/lib/typescript/index.d.ts +1 -0
  34. package/package.json +1 -1
  35. package/src/WeatherLayerManager.js +2024 -2004
  36. package/src/aguaceroRnDebug.js +8 -1
  37. package/src/gridCdnAuth.js +56 -0
  38. package/src/index.js +19 -15
  39. package/src/nexrad/nexradAndroidController.js +1078 -1078
  40. package/src/nexrad/nexradDiag.js +150 -150
  41. package/src/satellite/satelliteAndroidController.js +245 -245
@@ -1,245 +1,245 @@
1
- /**
2
- * AguaceroCore satellite mode → Android native {@link SatelliteLayerView} (parity with mapsgl
3
- * {@link WeatherLayerManager} satellite paths — preload timeline + scrub via {@code targetUnix}).
4
- */
5
- import { resolveSatelliteS3FileName } from '@aguacerowx/javascript-sdk';
6
- import { satBridgeWarn } from '../satelliteBridgeDiag';
7
- import { aguaceroDebug, getAguaceroAuthDiagnosticSnapshot, isAguaceroRnDebugEnabled, redactApiKeyFromUrl } from '../aguaceroRnDebug';
8
-
9
- /**
10
- * Target frame first, then remaining frames by increasing temporal distance to {@code targetUnix}.
11
- * @param {{ unix: number; url: string; shaderFileName: string }[]} frames
12
- * @param {number | null} targetUnix
13
- */
14
- export function sortFramesByTargetProximity(frames, targetUnix) {
15
- if (!frames.length) return frames;
16
- if (targetUnix == null || !Number.isFinite(targetUnix)) {
17
- return frames;
18
- }
19
- const withDistance = frames.map((f) => ({
20
- ...f,
21
- _d: Math.abs(f.unix - targetUnix),
22
- }));
23
- withDistance.sort((a, b) => {
24
- if (a._d !== b._d) return a._d - b._d;
25
- return a.unix - b.unix;
26
- });
27
- return withDistance.map(({ _d, ...rest }) => rest);
28
- }
29
-
30
- /**
31
- * @param {import('@aguacerowx/javascript-sdk').AguaceroCoreState} state
32
- * @param {number} satelliteTimestamp
33
- */
34
- export function buildSatelliteFetchParts(state, satelliteTimestamp) {
35
- const fileName = resolveSatelliteS3FileName(
36
- state.satelliteChannel,
37
- state.satelliteTimeToFileMap,
38
- satelliteTimestamp,
39
- );
40
- if (!fileName) {
41
- return null;
42
- }
43
- const channelName = state.satelliteChannel || state.variable;
44
- const channelNameUpper = String(channelName).toUpperCase();
45
- let s3FileName = fileName.replace('MULTI', channelNameUpper);
46
- if (!s3FileName.endsWith('.ktx2')) {
47
- s3FileName += '.ktx2';
48
- }
49
- let shaderFileName = s3FileName;
50
- if (!channelNameUpper.startsWith('C') || channelNameUpper.length > 3) {
51
- const token = channelNameUpper.toLowerCase().replace(/_/g, '');
52
- shaderFileName = `${s3FileName}_${token}_`;
53
- }
54
- const apiKey = state.apiKey;
55
- if (!apiKey) {
56
- return null;
57
- }
58
- const url = `https://d3dc62msmxkrd7.cloudfront.net/satellite/${s3FileName}?userId=${encodeURIComponent('sdk-user')}&apiKey=${encodeURIComponent(apiKey)}`;
59
- return { url, shaderFileName, frameKey: Number(satelliteTimestamp), apiKey };
60
- }
61
-
62
- export class SatelliteAndroidController {
63
- /**
64
- * @param {import('@aguacerowx/javascript-sdk').AguaceroCore} core
65
- * @param {React.RefObject<{ syncSatellite?: (json: string) => void; clearSatellite?: () => void; activateSatelliteCachedUnix?: (unix: number) => void; updateSatelliteStyle?: (json: string) => void }>} satelliteLayerRef
66
- */
67
- constructor(core, satelliteLayerRef) {
68
- this.core = core;
69
- this.satelliteLayerRef = satelliteLayerRef;
70
- this._destroyed = false;
71
- /** @type {string | undefined} */
72
- this._cachedRunKey = undefined;
73
- /** @type {string | undefined} */
74
- this._cachedTimelineSig = undefined;
75
- /** @type {number | null | undefined} */
76
- this._lastTargetUnix = undefined;
77
- /** @type {string | undefined} */
78
- this._lastStyleJson = undefined;
79
- }
80
-
81
- destroy() {
82
- this._destroyed = true;
83
- this._cachedRunKey = undefined;
84
- this._cachedTimelineSig = undefined;
85
- this._lastTargetUnix = undefined;
86
- this._lastStyleJson = undefined;
87
- const native = this.satelliteLayerRef?.current;
88
- native?.clearSatellite?.();
89
- }
90
-
91
- /**
92
- * @param {import('@aguacerowx/javascript-sdk').AguaceroCoreState} state
93
- */
94
- sync(state) {
95
- satBridgeWarn('SatelliteAndroidController.sync enter', {
96
- destroyed: this._destroyed,
97
- isSatellite: Boolean(state?.isSatellite),
98
- instrumentId: state?.satelliteInstrumentId ?? null,
99
- sector: state?.satelliteSectorLabel ?? null,
100
- channel: state?.satelliteChannel ?? null,
101
- tlMapSize:
102
- state?.satelliteTimeToFileMap && typeof state.satelliteTimeToFileMap === 'object'
103
- ? Object.keys(state.satelliteTimeToFileMap).length
104
- : 0,
105
- });
106
-
107
- if (this._destroyed || !state?.isSatellite) {
108
- satBridgeWarn('SatelliteAndroidController.sync early-exit', {
109
- destroyed: this._destroyed,
110
- isSatellite: state?.isSatellite,
111
- });
112
- return;
113
- }
114
-
115
- const native = this.satelliteLayerRef?.current;
116
- if (!native?.syncSatellite) {
117
- satBridgeWarn('SatelliteAndroidController.sync abort: no native.syncSatellite', {
118
- refExists: Boolean(this.satelliteLayerRef?.current),
119
- keys: this.satelliteLayerRef?.current ? Object.keys(this.satelliteLayerRef.current) : [],
120
- });
121
- return;
122
- }
123
-
124
- const keys = Object.keys(state.satelliteTimeToFileMap || {}).sort((a, b) => Number(a) - Number(b));
125
- const satRunKey = `${state.satelliteInstrumentId}|${state.satelliteSectorLabel}|${state.satelliteChannel}|${state.variable || ''}`;
126
- const timelineSig = keys.join(',');
127
-
128
- const mergedState = {
129
- ...state,
130
- apiKey: state.apiKey || this.core.apiKey,
131
- };
132
-
133
- const frames = [];
134
- for (const k of keys) {
135
- const ts = Number(k);
136
- if (!Number.isFinite(ts)) continue;
137
- const parts = buildSatelliteFetchParts(mergedState, ts);
138
- if (!parts) continue;
139
- frames.push({
140
- unix: ts,
141
- url: parts.url,
142
- shaderFileName: parts.shaderFileName,
143
- });
144
- }
145
-
146
- if (frames.length === 0) {
147
- console.warn('[AguaceroWX][satellite][satelliteSyncNoFrames]', {
148
- satRunKey,
149
- timelineKeyCount: keys.length,
150
- hasApiKey: Boolean(mergedState.apiKey),
151
- satelliteChannel: mergedState.satelliteChannel,
152
- mapKeys:
153
- mergedState.satelliteTimeToFileMap &&
154
- typeof mergedState.satelliteTimeToFileMap === 'object'
155
- ? Object.keys(mergedState.satelliteTimeToFileMap).length
156
- : 0,
157
- });
158
- }
159
-
160
- let targetUnix =
161
- state.satelliteTimestamp == null || state.satelliteTimestamp === undefined
162
- ? null
163
- : Number(state.satelliteTimestamp);
164
-
165
- // Core often omits satelliteTimestamp on the first satellite emit; without targetUnix the native
166
- // layer never promotes uploads to activeUnix until a later activateSatelliteCachedUnix — blank map.
167
- if (targetUnix == null || !Number.isFinite(targetUnix)) {
168
- const nk = keys.map((k) => Number(k)).filter((n) => Number.isFinite(n));
169
- if (nk.length > 0) {
170
- targetUnix = Math.max(...nk);
171
- }
172
- }
173
-
174
- const stylePayload = {
175
- visible: state.visible !== false,
176
- opacity: state.opacity ?? 1,
177
- fillSmoothing: 0,
178
- };
179
- const styleJson = JSON.stringify(stylePayload);
180
-
181
- const runKeyChanged = satRunKey !== this._cachedRunKey;
182
- const timelineChanged = timelineSig !== this._cachedTimelineSig;
183
-
184
- if (runKeyChanged || timelineChanged) {
185
- if (isAguaceroRnDebugEnabled()) {
186
- aguaceroDebug('satellite.sync.payload', {
187
- auth: getAguaceroAuthDiagnosticSnapshot(this.core),
188
- frameCount: frames.length,
189
- sampleFrameUrl: frames[0] ? redactApiKeyFromUrl(frames[0].url) : null,
190
- gridRequestSiteOrigin: mergedState.gridRequestSiteOrigin ?? this.core.gridRequestSiteOrigin ?? null,
191
- });
192
- }
193
- const sortedFrames = sortFramesByTargetProximity(frames, targetUnix);
194
- const payload = {
195
- runKey: satRunKey,
196
- // Must match query params on each frame URL (same merge as buildSatelliteFetchParts) and mapsgl fetch headers.
197
- apiKey: mergedState.apiKey || '',
198
- userId: 'sdk-user',
199
- // RN has no browser default Origin; native satellite fetch must mirror WeatherFrameProcessor / AguaceroCore grid headers.
200
- gridRequestSiteOrigin:
201
- typeof this.core.gridRequestSiteOrigin === 'string' ? this.core.gridRequestSiteOrigin : '',
202
- bundleId: this.core.bundleId || '',
203
- visible: stylePayload.visible,
204
- opacity: stylePayload.opacity,
205
- fillSmoothing: stylePayload.fillSmoothing,
206
- frames: sortedFrames,
207
- };
208
- if (targetUnix != null && Number.isFinite(targetUnix)) {
209
- payload.targetUnix = targetUnix;
210
- }
211
-
212
- const payloadJson = JSON.stringify(payload);
213
- native.syncSatellite(payloadJson);
214
- satBridgeWarn('SatelliteAndroidController.sync full payload sent', {
215
- runKey: satRunKey,
216
- timelineChanged,
217
- frameCount: sortedFrames.length,
218
- targetUnix: payload.targetUnix ?? null,
219
- jsonChars: payloadJson.length,
220
- });
221
- this._cachedRunKey = satRunKey;
222
- this._cachedTimelineSig = timelineSig;
223
- this._lastTargetUnix = targetUnix != null && Number.isFinite(targetUnix) ? targetUnix : null;
224
- this._lastStyleJson = styleJson;
225
- return;
226
- }
227
-
228
- // Same timeline: avoid large JSON over the bridge — scrub + style deltas only.
229
- const finiteTarget =
230
- targetUnix != null && Number.isFinite(targetUnix) ? targetUnix : null;
231
- if (finiteTarget !== this._lastTargetUnix) {
232
- if (finiteTarget != null && native.activateSatelliteCachedUnix) {
233
- native.activateSatelliteCachedUnix(finiteTarget);
234
- satBridgeWarn('SatelliteAndroidController.activateSatelliteCachedUnix', { unix: finiteTarget });
235
- }
236
- this._lastTargetUnix = finiteTarget;
237
- }
238
-
239
- if (styleJson !== this._lastStyleJson && native.updateSatelliteStyle) {
240
- native.updateSatelliteStyle(styleJson);
241
- satBridgeWarn('SatelliteAndroidController.updateSatelliteStyle', { jsonChars: styleJson.length });
242
- this._lastStyleJson = styleJson;
243
- }
244
- }
245
- }
1
+ /**
2
+ * AguaceroCore satellite mode → Android native {@link SatelliteLayerView} (parity with mapsgl
3
+ * {@link WeatherLayerManager} satellite paths — preload timeline + scrub via {@code targetUnix}).
4
+ */
5
+ import { resolveSatelliteS3FileName } from '@aguacerowx/javascript-sdk';
6
+ import { satBridgeWarn } from '../satelliteBridgeDiag';
7
+ import { aguaceroDebug, getAguaceroAuthDiagnosticSnapshot, isAguaceroRnDebugEnabled, redactApiKeyFromUrl } from '../aguaceroRnDebug';
8
+
9
+ /**
10
+ * Target frame first, then remaining frames by increasing temporal distance to {@code targetUnix}.
11
+ * @param {{ unix: number; url: string; shaderFileName: string }[]} frames
12
+ * @param {number | null} targetUnix
13
+ */
14
+ export function sortFramesByTargetProximity(frames, targetUnix) {
15
+ if (!frames.length) return frames;
16
+ if (targetUnix == null || !Number.isFinite(targetUnix)) {
17
+ return frames;
18
+ }
19
+ const withDistance = frames.map((f) => ({
20
+ ...f,
21
+ _d: Math.abs(f.unix - targetUnix),
22
+ }));
23
+ withDistance.sort((a, b) => {
24
+ if (a._d !== b._d) return a._d - b._d;
25
+ return a.unix - b.unix;
26
+ });
27
+ return withDistance.map(({ _d, ...rest }) => rest);
28
+ }
29
+
30
+ /**
31
+ * @param {import('@aguacerowx/javascript-sdk').AguaceroCoreState} state
32
+ * @param {number} satelliteTimestamp
33
+ */
34
+ export function buildSatelliteFetchParts(state, satelliteTimestamp) {
35
+ const fileName = resolveSatelliteS3FileName(
36
+ state.satelliteChannel,
37
+ state.satelliteTimeToFileMap,
38
+ satelliteTimestamp,
39
+ );
40
+ if (!fileName) {
41
+ return null;
42
+ }
43
+ const channelName = state.satelliteChannel || state.variable;
44
+ const channelNameUpper = String(channelName).toUpperCase();
45
+ let s3FileName = fileName.replace('MULTI', channelNameUpper);
46
+ if (!s3FileName.endsWith('.ktx2')) {
47
+ s3FileName += '.ktx2';
48
+ }
49
+ let shaderFileName = s3FileName;
50
+ if (!channelNameUpper.startsWith('C') || channelNameUpper.length > 3) {
51
+ const token = channelNameUpper.toLowerCase().replace(/_/g, '');
52
+ shaderFileName = `${s3FileName}_${token}_`;
53
+ }
54
+ const apiKey = state.apiKey;
55
+ if (!apiKey) {
56
+ return null;
57
+ }
58
+ const url = `https://d3dc62msmxkrd7.cloudfront.net/satellite/${s3FileName}?userId=${encodeURIComponent('sdk-user')}&apiKey=${encodeURIComponent(apiKey)}`;
59
+ return { url, shaderFileName, frameKey: Number(satelliteTimestamp), apiKey };
60
+ }
61
+
62
+ export class SatelliteAndroidController {
63
+ /**
64
+ * @param {import('@aguacerowx/javascript-sdk').AguaceroCore} core
65
+ * @param {React.RefObject<{ syncSatellite?: (json: string) => void; clearSatellite?: () => void; activateSatelliteCachedUnix?: (unix: number) => void; updateSatelliteStyle?: (json: string) => void }>} satelliteLayerRef
66
+ */
67
+ constructor(core, satelliteLayerRef) {
68
+ this.core = core;
69
+ this.satelliteLayerRef = satelliteLayerRef;
70
+ this._destroyed = false;
71
+ /** @type {string | undefined} */
72
+ this._cachedRunKey = undefined;
73
+ /** @type {string | undefined} */
74
+ this._cachedTimelineSig = undefined;
75
+ /** @type {number | null | undefined} */
76
+ this._lastTargetUnix = undefined;
77
+ /** @type {string | undefined} */
78
+ this._lastStyleJson = undefined;
79
+ }
80
+
81
+ destroy() {
82
+ this._destroyed = true;
83
+ this._cachedRunKey = undefined;
84
+ this._cachedTimelineSig = undefined;
85
+ this._lastTargetUnix = undefined;
86
+ this._lastStyleJson = undefined;
87
+ const native = this.satelliteLayerRef?.current;
88
+ native?.clearSatellite?.();
89
+ }
90
+
91
+ /**
92
+ * @param {import('@aguacerowx/javascript-sdk').AguaceroCoreState} state
93
+ */
94
+ sync(state) {
95
+ satBridgeWarn('SatelliteAndroidController.sync enter', {
96
+ destroyed: this._destroyed,
97
+ isSatellite: Boolean(state?.isSatellite),
98
+ instrumentId: state?.satelliteInstrumentId ?? null,
99
+ sector: state?.satelliteSectorLabel ?? null,
100
+ channel: state?.satelliteChannel ?? null,
101
+ tlMapSize:
102
+ state?.satelliteTimeToFileMap && typeof state.satelliteTimeToFileMap === 'object'
103
+ ? Object.keys(state.satelliteTimeToFileMap).length
104
+ : 0,
105
+ });
106
+
107
+ if (this._destroyed || !state?.isSatellite) {
108
+ satBridgeWarn('SatelliteAndroidController.sync early-exit', {
109
+ destroyed: this._destroyed,
110
+ isSatellite: state?.isSatellite,
111
+ });
112
+ return;
113
+ }
114
+
115
+ const native = this.satelliteLayerRef?.current;
116
+ if (!native?.syncSatellite) {
117
+ satBridgeWarn('SatelliteAndroidController.sync abort: no native.syncSatellite', {
118
+ refExists: Boolean(this.satelliteLayerRef?.current),
119
+ keys: this.satelliteLayerRef?.current ? Object.keys(this.satelliteLayerRef.current) : [],
120
+ });
121
+ return;
122
+ }
123
+
124
+ const keys = Object.keys(state.satelliteTimeToFileMap || {}).sort((a, b) => Number(a) - Number(b));
125
+ const satRunKey = `${state.satelliteInstrumentId}|${state.satelliteSectorLabel}|${state.satelliteChannel}|${state.variable || ''}`;
126
+ const timelineSig = keys.join(',');
127
+
128
+ const mergedState = {
129
+ ...state,
130
+ apiKey: state.apiKey || this.core.apiKey,
131
+ };
132
+
133
+ const frames = [];
134
+ for (const k of keys) {
135
+ const ts = Number(k);
136
+ if (!Number.isFinite(ts)) continue;
137
+ const parts = buildSatelliteFetchParts(mergedState, ts);
138
+ if (!parts) continue;
139
+ frames.push({
140
+ unix: ts,
141
+ url: parts.url,
142
+ shaderFileName: parts.shaderFileName,
143
+ });
144
+ }
145
+
146
+ if (frames.length === 0) {
147
+ console.warn('[AguaceroWX][satellite][satelliteSyncNoFrames]', {
148
+ satRunKey,
149
+ timelineKeyCount: keys.length,
150
+ hasApiKey: Boolean(mergedState.apiKey),
151
+ satelliteChannel: mergedState.satelliteChannel,
152
+ mapKeys:
153
+ mergedState.satelliteTimeToFileMap &&
154
+ typeof mergedState.satelliteTimeToFileMap === 'object'
155
+ ? Object.keys(mergedState.satelliteTimeToFileMap).length
156
+ : 0,
157
+ });
158
+ }
159
+
160
+ let targetUnix =
161
+ state.satelliteTimestamp == null || state.satelliteTimestamp === undefined
162
+ ? null
163
+ : Number(state.satelliteTimestamp);
164
+
165
+ // Core often omits satelliteTimestamp on the first satellite emit; without targetUnix the native
166
+ // layer never promotes uploads to activeUnix until a later activateSatelliteCachedUnix — blank map.
167
+ if (targetUnix == null || !Number.isFinite(targetUnix)) {
168
+ const nk = keys.map((k) => Number(k)).filter((n) => Number.isFinite(n));
169
+ if (nk.length > 0) {
170
+ targetUnix = Math.max(...nk);
171
+ }
172
+ }
173
+
174
+ const stylePayload = {
175
+ visible: state.visible !== false,
176
+ opacity: state.opacity ?? 1,
177
+ fillSmoothing: 0,
178
+ };
179
+ const styleJson = JSON.stringify(stylePayload);
180
+
181
+ const runKeyChanged = satRunKey !== this._cachedRunKey;
182
+ const timelineChanged = timelineSig !== this._cachedTimelineSig;
183
+
184
+ if (runKeyChanged || timelineChanged) {
185
+ if (isAguaceroRnDebugEnabled()) {
186
+ aguaceroDebug('satellite.sync.payload', {
187
+ auth: getAguaceroAuthDiagnosticSnapshot(this.core),
188
+ frameCount: frames.length,
189
+ sampleFrameUrl: frames[0] ? redactApiKeyFromUrl(frames[0].url) : null,
190
+ gridRequestSiteOrigin: mergedState.gridRequestSiteOrigin ?? this.core.gridRequestSiteOrigin ?? null,
191
+ });
192
+ }
193
+ const sortedFrames = sortFramesByTargetProximity(frames, targetUnix);
194
+ const payload = {
195
+ runKey: satRunKey,
196
+ // Must match query params on each frame URL (same merge as buildSatelliteFetchParts) and mapsgl fetch headers.
197
+ apiKey: mergedState.apiKey || '',
198
+ userId: 'sdk-user',
199
+ // RN has no browser default Origin; native satellite fetch must mirror WeatherFrameProcessor / AguaceroCore grid headers.
200
+ gridRequestSiteOrigin:
201
+ typeof this.core.gridRequestSiteOrigin === 'string' ? this.core.gridRequestSiteOrigin : '',
202
+ bundleId: this.core.bundleId || '',
203
+ visible: stylePayload.visible,
204
+ opacity: stylePayload.opacity,
205
+ fillSmoothing: stylePayload.fillSmoothing,
206
+ frames: sortedFrames,
207
+ };
208
+ if (targetUnix != null && Number.isFinite(targetUnix)) {
209
+ payload.targetUnix = targetUnix;
210
+ }
211
+
212
+ const payloadJson = JSON.stringify(payload);
213
+ native.syncSatellite(payloadJson);
214
+ satBridgeWarn('SatelliteAndroidController.sync full payload sent', {
215
+ runKey: satRunKey,
216
+ timelineChanged,
217
+ frameCount: sortedFrames.length,
218
+ targetUnix: payload.targetUnix ?? null,
219
+ jsonChars: payloadJson.length,
220
+ });
221
+ this._cachedRunKey = satRunKey;
222
+ this._cachedTimelineSig = timelineSig;
223
+ this._lastTargetUnix = targetUnix != null && Number.isFinite(targetUnix) ? targetUnix : null;
224
+ this._lastStyleJson = styleJson;
225
+ return;
226
+ }
227
+
228
+ // Same timeline: avoid large JSON over the bridge — scrub + style deltas only.
229
+ const finiteTarget =
230
+ targetUnix != null && Number.isFinite(targetUnix) ? targetUnix : null;
231
+ if (finiteTarget !== this._lastTargetUnix) {
232
+ if (finiteTarget != null && native.activateSatelliteCachedUnix) {
233
+ native.activateSatelliteCachedUnix(finiteTarget);
234
+ satBridgeWarn('SatelliteAndroidController.activateSatelliteCachedUnix', { unix: finiteTarget });
235
+ }
236
+ this._lastTargetUnix = finiteTarget;
237
+ }
238
+
239
+ if (styleJson !== this._lastStyleJson && native.updateSatelliteStyle) {
240
+ native.updateSatelliteStyle(styleJson);
241
+ satBridgeWarn('SatelliteAndroidController.updateSatelliteStyle', { jsonChars: styleJson.length });
242
+ this._lastStyleJson = styleJson;
243
+ }
244
+ }
245
+ }