mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(layers): pause periodic network updates in hidden tabs
This commit is contained in:
1 parent
de2138e84c
commit
ec0985618c
7 files changed
+81
-18
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
# God's Eye View Current State
|
# God's Eye View Current State
|
||||||
|
|
||||||
|
Manager-owned periodic network updates pause while the document is hidden. Returning to a visible tab runs one overdue update per enabled layer without overlapping an active refresh; explicit refresh requests remain available. Transit also defers camera-driven feed admission while hidden and rechecks coverage on return. Visibility listeners are released with their polling intervals.
|
||||||
|
|
||||||
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
Shared distance checks use `src/geoDistance.js` with `{ lat, lon }` points and
|
||||||
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
the IUGG mean spherical Earth radius, 6371.0088 km. `greatCircleKm` is
|
||||||
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
canonical; `greatCircleMeters` is its exact metre conversion. Missing,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function createApplicationData({
|
|||||||
// Initialize data layer manager
|
// Initialize data layer manager
|
||||||
const dataManager = new LayerLifecycle(viewer, {
|
const dataManager = new LayerLifecycle(viewer, {
|
||||||
allowQaRegistration,
|
allowQaRegistration,
|
||||||
|
visibilityTarget: document,
|
||||||
getSourceAvailability: catalog?.getSourceAvailability,
|
getSourceAvailability: catalog?.getSourceAvailability,
|
||||||
});
|
});
|
||||||
defer(async () => {
|
defer(async () => {
|
||||||
|
|||||||
+40
-17
@@ -72,13 +72,20 @@ function refreshFailureFromStats(stats, label) {
|
|||||||
/**
|
/**
|
||||||
* LayerLifecycle — Manages registration, toggling, and update loops
|
* LayerLifecycle — Manages registration, toggling, and update loops
|
||||||
* for real-time data overlays on the CesiumJS globe.
|
* for real-time data overlays on the CesiumJS globe.
|
||||||
|
* Hosts may inject a visibilityTarget with hidden/visibilityState and event
|
||||||
|
* listener methods. Without one, periodic work retains its headless behavior.
|
||||||
*/
|
*/
|
||||||
export class LayerLifecycle {
|
export class LayerLifecycle {
|
||||||
constructor(
|
constructor(
|
||||||
viewer,
|
viewer,
|
||||||
{ allowQaRegistration = false, getSourceAvailability } = {},
|
{
|
||||||
|
allowQaRegistration = false,
|
||||||
|
getSourceAvailability,
|
||||||
|
visibilityTarget = null,
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
this._catalogSourceAvailability = getSourceAvailability;
|
this._catalogSourceAvailability = getSourceAvailability;
|
||||||
|
this._visibilityTarget = visibilityTarget;
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
this._activityListeners = new Set();
|
this._activityListeners = new Set();
|
||||||
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
this.layers = new Map(); // id → { module, enabled, initialized, intervalId, lifecycleState, lifecycleUncertain }
|
||||||
@@ -528,6 +535,13 @@ export class LayerLifecycle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_stopUpdateLoop(entry) {
|
||||||
|
clearInterval(entry.intervalId);
|
||||||
|
entry.intervalId = null;
|
||||||
|
entry.removeVisibilityListener?.();
|
||||||
|
entry.removeVisibilityListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
_armUpdateLoop(layerId, entry) {
|
_armUpdateLoop(layerId, entry) {
|
||||||
const configuredRefreshInterval = Number(entry.module.refreshInterval);
|
const configuredRefreshInterval = Number(entry.module.refreshInterval);
|
||||||
const updateInterval = Number(entry.module.updateInterval);
|
const updateInterval = Number(entry.module.updateInterval);
|
||||||
@@ -538,9 +552,25 @@ export class LayerLifecycle {
|
|||||||
? updateInterval
|
? updateInterval
|
||||||
: 0;
|
: 0;
|
||||||
if (refreshInterval > 0) {
|
if (refreshInterval > 0) {
|
||||||
entry.intervalId = setInterval(() => {
|
const visibilityDocument = this._visibilityTarget;
|
||||||
|
let dueAt = Date.now() + refreshInterval;
|
||||||
|
const tick = () => {
|
||||||
|
if (
|
||||||
|
visibilityDocument?.hidden ||
|
||||||
|
visibilityDocument?.visibilityState === 'hidden' ||
|
||||||
|
!entry.enabled ||
|
||||||
|
entry.destroying ||
|
||||||
|
entry.refreshing ||
|
||||||
|
Date.now() < dueAt
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
dueAt = Date.now() + refreshInterval;
|
||||||
void this._runPeriodicUpdate(layerId, entry);
|
void this._runPeriodicUpdate(layerId, entry);
|
||||||
}, refreshInterval);
|
};
|
||||||
|
entry.intervalId = setInterval(tick, refreshInterval);
|
||||||
|
visibilityDocument?.addEventListener?.('visibilitychange', tick);
|
||||||
|
entry.removeVisibilityListener = () =>
|
||||||
|
visibilityDocument?.removeEventListener?.('visibilitychange', tick);
|
||||||
} else if (updateInterval === 0) {
|
} else if (updateInterval === 0) {
|
||||||
entry.intervalId = setInterval(() => {
|
entry.intervalId = setInterval(() => {
|
||||||
if (!entry.enabled) return;
|
if (!entry.enabled) return;
|
||||||
@@ -772,8 +802,7 @@ export class LayerLifecycle {
|
|||||||
entry.lifecycleUncertain = !compensated && !cleanupConfirmed;
|
entry.lifecycleUncertain = !compensated && !cleanupConfirmed;
|
||||||
settleLifecycle();
|
settleLifecycle();
|
||||||
if (!entry.enabled && entry.intervalId) {
|
if (!entry.enabled && entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
this._publishActivity({ type: 'status' });
|
this._publishActivity({ type: 'status' });
|
||||||
if (!compensated) {
|
if (!compensated) {
|
||||||
@@ -833,8 +862,7 @@ export class LayerLifecycle {
|
|||||||
}
|
}
|
||||||
if (signal?.aborted) return finishCancelledDisable('disable');
|
if (signal?.aborted) return finishCancelledDisable('disable');
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
entry.enabled = false;
|
entry.enabled = false;
|
||||||
entry.lifecycleUncertain = false;
|
entry.lifecycleUncertain = false;
|
||||||
@@ -845,8 +873,7 @@ export class LayerLifecycle {
|
|||||||
let abortCleanup = null;
|
let abortCleanup = null;
|
||||||
const cancelEnable = () => {
|
const cancelEnable = () => {
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
// Disable immediately so modules with their own AbortController (Radio)
|
// Disable immediately so modules with their own AbortController (Radio)
|
||||||
// cancel pending update work at the same turn boundary. A second
|
// cancel pending update work at the same turn boundary. A second
|
||||||
@@ -876,8 +903,7 @@ export class LayerLifecycle {
|
|||||||
// a later abort cannot revoke a successful retry.
|
// a later abort cannot revoke a successful retry.
|
||||||
signal?.removeEventListener('abort', cancelEnable);
|
signal?.removeEventListener('abort', cancelEnable);
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
await abortCleanup;
|
await abortCleanup;
|
||||||
let cleanupConfirmed = false;
|
let cleanupConfirmed = false;
|
||||||
@@ -918,8 +944,7 @@ export class LayerLifecycle {
|
|||||||
};
|
};
|
||||||
const finishFailedEnable = async (phase, error) => {
|
const finishFailedEnable = async (phase, error) => {
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
let cleanupConfirmed = false;
|
let cleanupConfirmed = false;
|
||||||
try {
|
try {
|
||||||
@@ -1029,8 +1054,7 @@ export class LayerLifecycle {
|
|||||||
// Always clear any stale interval before assigning a new one, so we never
|
// Always clear any stale interval before assigning a new one, so we never
|
||||||
// orphan a running timer and end up double-polling.
|
// orphan a running timer and end up double-polling.
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager-owned periodic refresh work has one normalized loading/error
|
// Manager-owned periodic refresh work has one normalized loading/error
|
||||||
@@ -2158,8 +2182,7 @@ export class LayerLifecycle {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (entry.intervalId) {
|
if (entry.intervalId) {
|
||||||
clearInterval(entry.intervalId);
|
this._stopUpdateLoop(entry);
|
||||||
entry.intervalId = null;
|
|
||||||
}
|
}
|
||||||
entry.enabled = false;
|
entry.enabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ export { layerFeedState } from '../ui/layers.js';
|
|||||||
/** Compatibility facade for callers that construct a manager with its panel. */
|
/** Compatibility facade for callers that construct a manager with its panel. */
|
||||||
export class DataLayerManager extends LayerLifecycle {
|
export class DataLayerManager extends LayerLifecycle {
|
||||||
constructor(viewer, options) {
|
constructor(viewer, options) {
|
||||||
super(viewer, options);
|
super(viewer, { visibilityTarget: globalThis.document, ...options });
|
||||||
this._presentation = new LayerPresentation(this);
|
this._presentation = new LayerPresentation(this);
|
||||||
}
|
}
|
||||||
buildTogglePanel(container) {
|
buildTogglePanel(container) {
|
||||||
|
|||||||
@@ -3492,3 +3492,34 @@ test('refreshLayerStats reaches presentation through the bare lifecycle', async
|
|||||||
lifecycle.refreshLayerStats();
|
lifecycle.refreshLayerStats();
|
||||||
assert.deepEqual(changes, ['status']);
|
assert.deepEqual(changes, ['status']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('periodic polls pause hidden, catch up once when due, and dispose visibility listeners', async (t) => {
|
||||||
|
const listeners = new Set();
|
||||||
|
const doc = {
|
||||||
|
hidden: false,
|
||||||
|
addEventListener: (_name, fn) => listeners.add(fn),
|
||||||
|
removeEventListener: (_name, fn) => listeners.delete(fn),
|
||||||
|
};
|
||||||
|
t.mock.timers.enable({ apis: ['setInterval', 'Date'], now: 1000 });
|
||||||
|
const { LayerLifecycle } = await import('./lifecycle.js');
|
||||||
|
const manager = new LayerLifecycle({}, { visibilityTarget: doc });
|
||||||
|
const { module, calls } = makeSlowLayer('hidden-poll');
|
||||||
|
manager.register(module);
|
||||||
|
await manager.setEnabled('hidden-poll', true);
|
||||||
|
assert.equal(calls.update, 1);
|
||||||
|
doc.hidden = true;
|
||||||
|
t.mock.timers.tick(10000);
|
||||||
|
assert.equal(calls.update, 1);
|
||||||
|
assert.equal(await manager.refreshLayer('hidden-poll'), true);
|
||||||
|
assert.equal(calls.update, 2, 'explicit refresh remains allowed');
|
||||||
|
doc.hidden = false;
|
||||||
|
for (const fn of listeners) fn();
|
||||||
|
for (const fn of listeners) fn();
|
||||||
|
assert.equal(calls.update, 3);
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
await manager.destroyAll();
|
||||||
|
assert.equal(listeners.size, 0);
|
||||||
|
t.mock.timers.tick(10000);
|
||||||
|
assert.equal(calls.update, 3);
|
||||||
|
});
|
||||||
@@ -97,6 +97,7 @@ export function createLifecycle({ state, services, parts }) {
|
|||||||
else if (hidden) {
|
else if (hidden) {
|
||||||
hidden = false;
|
hidden = false;
|
||||||
parts.rendering.resumePlayback();
|
parts.rendering.resumePlayback();
|
||||||
|
parts.viewport.runProximityCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function bindVisibility() {
|
function bindVisibility() {
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ export function createViewport({ state, services, parts }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runProximityCheck() {
|
function runProximityCheck() {
|
||||||
|
if (
|
||||||
|
globalThis.document?.hidden ||
|
||||||
|
globalThis.document?.visibilityState === 'hidden'
|
||||||
|
)
|
||||||
|
return;
|
||||||
if (!state._enabled || !state._viewer) return;
|
if (!state._enabled || !state._viewer) return;
|
||||||
refreshViewBounds();
|
refreshViewBounds();
|
||||||
const altitude = getCameraAltitude();
|
const altitude = getCameraAltitude();
|
||||||
|
|||||||
Reference in new issue
Block a user