mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Share links carry the world time when made away from live
Field 28, worldTime, holds the world time of a link made away from live: zigzag seconds since 2026-01-01, like field 8, but it may lie ahead of the link's creation (a forecast moment). Opening such a link pauses the world clock at that time; a link without it leaves the clock alone, so a page opens live and Back or Forward does not stop a replay. Times no JS Date can hold are dropped.
This commit is contained in:
1 parent
0c4da3cdb2
commit
32ec6ed436
8 files changed
+96
-8
No files matched your search
@@ -52,7 +52,8 @@ const utc = (value) =>
|
||||
: t('Unavailable');
|
||||
|
||||
/** A per-application observation layer, using the existing layer lifecycle and
|
||||
* row controls. History is transient: shared links always open latest imagery. */
|
||||
* row controls. Its time is the world clock's, so a link made away from live
|
||||
* opens its imagery at that time; one made live opens the latest. */
|
||||
export function createWeatherLayer({
|
||||
feed,
|
||||
clock,
|
||||
|
||||
+14
-2
@@ -31,6 +31,7 @@ import {
|
||||
} from '../data/detectionPolicy.js';
|
||||
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
|
||||
import { VIEW_STYLES } from '../view/index.js';
|
||||
import { isWorldTime } from '../time/worldClock.js';
|
||||
import { RETIRED_STYLES } from '../visualStylePolicy.js';
|
||||
import {
|
||||
GLOBAL_POST_DEFAULTS,
|
||||
@@ -373,6 +374,7 @@ export const SHARE_VIEW_FIELDS = freezeAll([
|
||||
{ number: 25, key: 'scopeFeatherPct', type: 'uint', default: 100 },
|
||||
{ number: 26, key: 'scopeTerminusPct', type: 'uint', default: null },
|
||||
{ number: 27, key: 'styleParams', type: 'bytes', default: BY_STYLE },
|
||||
{ number: 28, key: 'worldTime', type: 'epoch', default: null },
|
||||
]);
|
||||
const FIELD_BY_KEY = new Map(SHARE_VIEW_FIELDS.map((f) => [f.key, f]));
|
||||
const fieldNumber = (key) => FIELD_BY_KEY.get(key).number;
|
||||
@@ -732,6 +734,9 @@ export function normalizeShareViewState(input = {}) {
|
||||
: null;
|
||||
state.panelState = normalizePanelState(source.panelState);
|
||||
state.sharedAtMs = epochMsOr(source.sharedAtMs);
|
||||
state.worldTime = isWorldTime(source.worldTime)
|
||||
? Math.floor(source.worldTime / 1000) * 1000
|
||||
: null;
|
||||
let view = normalizeView(source.view);
|
||||
if (state.layerState) view = liftTrackedEntity(state.layerState, view);
|
||||
if (view.kind === 'thing') {
|
||||
@@ -948,8 +953,9 @@ export function encodeShareView(input) {
|
||||
writer.sintField(field.number, signedAngle(state[field.key]));
|
||||
break;
|
||||
case 'sharedAtMs':
|
||||
if (state.sharedAtMs !== null)
|
||||
writer.sintField(field.number, epochSeconds(state.sharedAtMs));
|
||||
case 'worldTime':
|
||||
if (state[field.key] !== null)
|
||||
writer.sintField(field.number, epochSeconds(state[field.key]));
|
||||
break;
|
||||
case 'flags': {
|
||||
let bits = 0;
|
||||
@@ -1347,6 +1353,12 @@ export function decodeShareView(code, { nowMs = Date.now() } = {}) {
|
||||
raw.sharedAtMs =
|
||||
createdAtS >= 1 && Number.isSafeInteger(ms) && ms <= nowMs ? ms : null;
|
||||
}
|
||||
// The world time the link was made at, when it was away from live. It
|
||||
// may lie ahead of the link's creation (a forecast) or of now.
|
||||
if (get('worldTime')) {
|
||||
const ms = (SHARE_VIEW_EPOCH_S + unz(get('worldTime').varint)) * 1000;
|
||||
raw.worldTime = isWorldTime(ms) ? ms : null;
|
||||
}
|
||||
|
||||
if (get('layerState'))
|
||||
raw.layerState = decodeLayers(
|
||||
|
||||
@@ -361,6 +361,7 @@ test('every registered field round-trips at once', () => {
|
||||
})),
|
||||
},
|
||||
sharedAtMs: NOW_MS,
|
||||
worldTime: NOW_MS - 6 * 3_600_000,
|
||||
};
|
||||
// A thing view derives its camera (roll 0), so the camera fields are
|
||||
// checked on the same state as a place view.
|
||||
@@ -501,6 +502,7 @@ test('the field registry is well formed and permanent', () => {
|
||||
[25, 'scopeFeatherPct', 100],
|
||||
[26, 'scopeTerminusPct', null],
|
||||
[27, 'styleParams', BY_STYLE],
|
||||
[28, 'worldTime', null],
|
||||
],
|
||||
);
|
||||
assert.deepEqual(
|
||||
@@ -1338,3 +1340,22 @@ test('a layer option filed under a mirror layer is ignored', () => {
|
||||
{ scope: 'layer-option', code: `${military}.${layerTokenNumber('m')}` },
|
||||
]);
|
||||
});
|
||||
|
||||
test('the world time is written only away from live, and may lie ahead', () => {
|
||||
const at = (worldTime) =>
|
||||
decodeShareView(encodeShareView({ sharedAtMs: NOW_MS, worldTime }), {
|
||||
nowMs: NOW_MS,
|
||||
}).state.worldTime;
|
||||
assert.equal(at(null), null);
|
||||
assert.equal(at(NOW_MS - 3_600_000), NOW_MS - 3_600_000);
|
||||
// A forecast moment ahead of the link's creation is kept.
|
||||
assert.equal(at(NOW_MS + 86_400_000), NOW_MS + 86_400_000);
|
||||
// Before the 2026 epoch too: the field is zigzag.
|
||||
assert.equal(at(Date.UTC(2024, 0, 1)), Date.UTC(2024, 0, 1));
|
||||
// Past what a Date can hold: dropped, so the link opens live.
|
||||
assert.equal(at(8.8e15), null);
|
||||
assert.ok(
|
||||
encodeShareView({ sharedAtMs: NOW_MS, worldTime: NOW_MS - 1000 }).length >
|
||||
encodeShareView({ sharedAtMs: NOW_MS }).length,
|
||||
);
|
||||
});
|
||||
+11
-2
@@ -5,6 +5,7 @@ import {
|
||||
normalizeAllocationStrategy,
|
||||
} from './data/detectionPolicy.js';
|
||||
import { clampScopeTerminusPct } from './scopeMask.js';
|
||||
import { worldClock as pageWorldClock } from './time/worldClock.js';
|
||||
import { clampScopeFeatherPct } from './scopeMaskPolicy.js';
|
||||
import {
|
||||
decodeLayerStateParams,
|
||||
@@ -68,9 +69,11 @@ export class ShareLinkManager {
|
||||
isNavigationCurrent,
|
||||
cancelOwnedNavigation,
|
||||
initialShareCode = null,
|
||||
worldClock = pageWorldClock,
|
||||
} = {},
|
||||
) {
|
||||
this.viewer = viewer;
|
||||
this._worldClock = worldClock;
|
||||
// A host may hand over the share code to open instead of the address.
|
||||
this._initialShareCode =
|
||||
typeof initialShareCode === 'string' ? initialShareCode : null;
|
||||
@@ -163,8 +166,9 @@ export class ShareLinkManager {
|
||||
|
||||
/**
|
||||
* The restore state for a share code, in the long form's parsed shape plus
|
||||
* `shareView` (what the view follows, with its offset and last-known fix),
|
||||
* or null when the code is refused.
|
||||
* `shareView` (what the view follows, with its offset and last-known fix)
|
||||
* and `worldTime` (null when the link was made live), or null when the
|
||||
* code is refused.
|
||||
*/
|
||||
_parseShareCode(code) {
|
||||
const result = decodeShareView(code);
|
||||
@@ -174,6 +178,7 @@ export class ShareLinkManager {
|
||||
}
|
||||
const state = toLegacyShareState(result.state);
|
||||
state.shareView = result.state.view;
|
||||
state.worldTime = result.state.worldTime;
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -592,6 +597,10 @@ export class ShareLinkManager {
|
||||
// that state in the share format.
|
||||
const state = fromLegacyShareState(this._parseShareHash(params.toString()));
|
||||
state.sharedAtMs = Math.floor(nowMs / 1000) * 1000;
|
||||
// Away from live, the link opens at the same world time.
|
||||
state.worldTime = this._worldClock.isLive()
|
||||
? null
|
||||
: Math.floor(this._worldClock.now() / 1000) * 1000;
|
||||
let subject = null;
|
||||
try {
|
||||
subject = this._followSubjectProvider?.({ nowMs }) ?? null;
|
||||
|
||||
@@ -423,3 +423,28 @@ test('the layer coordinator replaces in-flight share layers with the navigated s
|
||||
);
|
||||
coordinator.destroy();
|
||||
});
|
||||
|
||||
test('a link made away from live carries its world time to the page that opens it', (t) => {
|
||||
installTab(t, SHARE_A);
|
||||
const worldTime = Date.UTC(2026, 9, 30, 6, 15, 42);
|
||||
const sharer = new ShareLinkManager(fakeViewer(), {
|
||||
worldClock: { isLive: () => false, now: () => worldTime + 400 },
|
||||
});
|
||||
const link = sharer.buildLink({ nowMs: Date.UTC(2026, 10, 1) });
|
||||
sharer.destroy();
|
||||
const code = new URL(link).pathname.split('/').at(-1);
|
||||
const opener = new ShareLinkManager(fakeViewer(), { initialShareCode: code });
|
||||
assert.equal(opener.parseInitialHash().worldTime, worldTime);
|
||||
opener.destroy();
|
||||
|
||||
const live = new ShareLinkManager(fakeViewer(), {
|
||||
worldClock: { isLive: () => true, now: () => worldTime },
|
||||
});
|
||||
const liveCode = new URL(live.buildLink()).pathname.split('/').at(-1);
|
||||
live.destroy();
|
||||
const liveOpener = new ShareLinkManager(fakeViewer(), {
|
||||
initialShareCode: liveCode,
|
||||
});
|
||||
assert.equal(liveOpener.parseInitialHash().worldTime, null);
|
||||
liveOpener.destroy();
|
||||
});
|
||||
@@ -4,6 +4,16 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
||||
import { t, msg, formatDate } from '../i18n/index.js';
|
||||
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { isWorldTime, worldClock } from '../time/worldClock.js';
|
||||
|
||||
// A link made away from live opens at its world time, paused. One without a
|
||||
// world time leaves the clock as it is: a page opens live, and Back or Forward
|
||||
// through long-form hashes (which never carry one) does not stop a replay.
|
||||
function applySharedWorldTime(state) {
|
||||
if (!isWorldTime(state?.worldTime)) return;
|
||||
worldClock.pause();
|
||||
worldClock.seek(state.worldTime);
|
||||
}
|
||||
|
||||
/** How long a shared vessel may take to appear in the vessel feed. */
|
||||
export const SHARED_VESSEL_WAIT_MS = 120_000;
|
||||
@@ -146,6 +156,7 @@ export class ShareRestoration {
|
||||
: Promise.resolve({ status: 'not-requested', share: null, layers: [] });
|
||||
if (savedState) {
|
||||
this._hasShareState = true;
|
||||
applySharedWorldTime(savedState);
|
||||
// Reserve camera authority now; the delayed mesh-friendly flight may
|
||||
// run only if no newer user, voice, or tracking navigation has won.
|
||||
this._initialShareNavigationGeneration =
|
||||
@@ -301,6 +312,7 @@ export class ShareRestoration {
|
||||
layers: [],
|
||||
});
|
||||
this._initialShareState = state;
|
||||
applySharedWorldTime(state);
|
||||
this._initialShareSelectionSuperseded = false;
|
||||
this._hasShareState = true;
|
||||
this._navigatedShareRestoreActive = true;
|
||||
|
||||
Reference in new issue
Block a user