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:
Sameh Khamis committed 2026-11-01 21:50:09 -08:00
1 parent 0c4da3cdb2
commit 32ec6ed436
8 files changed
+96 -8

No files matched your search

+5
View File
@@ -119,6 +119,7 @@ probability of about 255 in 256 (many of them already fail as base64url).
| 25 | `scopeFeatherPct` | uint | 100 | 0–200 | | 25 | `scopeFeatherPct` | uint | 100 | 0–200 |
| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus | | 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus |
| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs | | 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs |
| 28 | `worldTime` | epoch | none (live) | zigzag seconds since 2026-01-01T00:00:00Z |
Field 1 absent means `thing` when field 13 is present and `place` otherwise, Field 1 absent means `thing` when field 13 is present and `place` otherwise,
so v1 never writes it; a later view kind writes its code. so v1 never writes it; a later view kind writes its code.
@@ -132,6 +133,10 @@ style also extends the pinned style-defaults table.
`hudVariant` code 1 (`operator`) is retired: the app no longer offers that `hudVariant` code 1 (`operator`) is retired: the app no longer offers that
layout and restores it as `tactical`. The code stays reserved. layout and restores it as `tactical`. The code stays reserved.
Field 28 is the world time the link was made at, written only when it was away
from live. A link without it opens live. Unlike field 8 it may lie ahead of the
link's creation (a forecast moment), and it opens paused at that time.
Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3 Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on). `celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
+6 -3
View File
@@ -1105,7 +1105,8 @@
"src/ui/kit/UiBadge.vue", "src/ui/kit/UiBadge.vue",
"src/ui/stores/globeActions.js", "src/ui/stores/globeActions.js",
"src/ui/stores/recording.js", "src/ui/stores/recording.js",
"src/ui/i18n.js" "src/ui/i18n.js",
"src/time/worldClock.js"
], ],
"external": ["cesium", "vue", "pinia", "vue-i18n"] "external": ["cesium", "vue", "pinia", "vue-i18n"]
}, },
@@ -3890,7 +3891,8 @@
"src/i18n/locales/ru.json", "src/i18n/locales/ru.json",
"src/i18n/locales/uk.json", "src/i18n/locales/uk.json",
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json" "src/i18n/locales/zh-Hant.json",
"src/time/worldClock.js"
], ],
"external": [] "external": []
}, },
@@ -4580,7 +4582,8 @@
"src/data/detectionPolicy.js", "src/data/detectionPolicy.js",
"src/scopeMaskPolicy.js", "src/scopeMaskPolicy.js",
"src/i18n/marker.js", "src/i18n/marker.js",
"src/visualStylePolicy.js" "src/visualStylePolicy.js",
"src/time/worldClock.js"
], ],
"external": [] "external": []
}, },
+2 -1
View File
@@ -52,7 +52,8 @@ const utc = (value) =>
: t('Unavailable'); : t('Unavailable');
/** A per-application observation layer, using the existing layer lifecycle and /** 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({ export function createWeatherLayer({
feed, feed,
clock, clock,
+14 -2
View File
@@ -31,6 +31,7 @@ import {
} from '../data/detectionPolicy.js'; } from '../data/detectionPolicy.js';
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js'; import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
import { VIEW_STYLES } from '../view/index.js'; import { VIEW_STYLES } from '../view/index.js';
import { isWorldTime } from '../time/worldClock.js';
import { RETIRED_STYLES } from '../visualStylePolicy.js'; import { RETIRED_STYLES } from '../visualStylePolicy.js';
import { import {
GLOBAL_POST_DEFAULTS, GLOBAL_POST_DEFAULTS,
@@ -373,6 +374,7 @@ export const SHARE_VIEW_FIELDS = freezeAll([
{ number: 25, key: 'scopeFeatherPct', type: 'uint', default: 100 }, { number: 25, key: 'scopeFeatherPct', type: 'uint', default: 100 },
{ number: 26, key: 'scopeTerminusPct', type: 'uint', default: null }, { number: 26, key: 'scopeTerminusPct', type: 'uint', default: null },
{ number: 27, key: 'styleParams', type: 'bytes', default: BY_STYLE }, { 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 FIELD_BY_KEY = new Map(SHARE_VIEW_FIELDS.map((f) => [f.key, f]));
const fieldNumber = (key) => FIELD_BY_KEY.get(key).number; const fieldNumber = (key) => FIELD_BY_KEY.get(key).number;
@@ -732,6 +734,9 @@ export function normalizeShareViewState(input = {}) {
: null; : null;
state.panelState = normalizePanelState(source.panelState); state.panelState = normalizePanelState(source.panelState);
state.sharedAtMs = epochMsOr(source.sharedAtMs); state.sharedAtMs = epochMsOr(source.sharedAtMs);
state.worldTime = isWorldTime(source.worldTime)
? Math.floor(source.worldTime / 1000) * 1000
: null;
let view = normalizeView(source.view); let view = normalizeView(source.view);
if (state.layerState) view = liftTrackedEntity(state.layerState, view); if (state.layerState) view = liftTrackedEntity(state.layerState, view);
if (view.kind === 'thing') { if (view.kind === 'thing') {
@@ -948,8 +953,9 @@ export function encodeShareView(input) {
writer.sintField(field.number, signedAngle(state[field.key])); writer.sintField(field.number, signedAngle(state[field.key]));
break; break;
case 'sharedAtMs': case 'sharedAtMs':
if (state.sharedAtMs !== null) case 'worldTime':
writer.sintField(field.number, epochSeconds(state.sharedAtMs)); if (state[field.key] !== null)
writer.sintField(field.number, epochSeconds(state[field.key]));
break; break;
case 'flags': { case 'flags': {
let bits = 0; let bits = 0;
@@ -1347,6 +1353,12 @@ export function decodeShareView(code, { nowMs = Date.now() } = {}) {
raw.sharedAtMs = raw.sharedAtMs =
createdAtS >= 1 && Number.isSafeInteger(ms) && ms <= nowMs ? ms : null; 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')) if (get('layerState'))
raw.layerState = decodeLayers( raw.layerState = decodeLayers(
+21
View File
@@ -361,6 +361,7 @@ test('every registered field round-trips at once', () => {
})), })),
}, },
sharedAtMs: NOW_MS, sharedAtMs: NOW_MS,
worldTime: NOW_MS - 6 * 3_600_000,
}; };
// A thing view derives its camera (roll 0), so the camera fields are // A thing view derives its camera (roll 0), so the camera fields are
// checked on the same state as a place view. // 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], [25, 'scopeFeatherPct', 100],
[26, 'scopeTerminusPct', null], [26, 'scopeTerminusPct', null],
[27, 'styleParams', BY_STYLE], [27, 'styleParams', BY_STYLE],
[28, 'worldTime', null],
], ],
); );
assert.deepEqual( assert.deepEqual(
@@ -1338,3 +1340,22 @@ test('a layer option filed under a mirror layer is ignored', () => {
{ scope: 'layer-option', code: `${military}.${layerTokenNumber('m')}` }, { 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
View File
@@ -5,6 +5,7 @@ import {
normalizeAllocationStrategy, normalizeAllocationStrategy,
} from './data/detectionPolicy.js'; } from './data/detectionPolicy.js';
import { clampScopeTerminusPct } from './scopeMask.js'; import { clampScopeTerminusPct } from './scopeMask.js';
import { worldClock as pageWorldClock } from './time/worldClock.js';
import { clampScopeFeatherPct } from './scopeMaskPolicy.js'; import { clampScopeFeatherPct } from './scopeMaskPolicy.js';
import { import {
decodeLayerStateParams, decodeLayerStateParams,
@@ -68,9 +69,11 @@ export class ShareLinkManager {
isNavigationCurrent, isNavigationCurrent,
cancelOwnedNavigation, cancelOwnedNavigation,
initialShareCode = null, initialShareCode = null,
worldClock = pageWorldClock,
} = {}, } = {},
) { ) {
this.viewer = viewer; this.viewer = viewer;
this._worldClock = worldClock;
// A host may hand over the share code to open instead of the address. // A host may hand over the share code to open instead of the address.
this._initialShareCode = this._initialShareCode =
typeof initialShareCode === 'string' ? initialShareCode : null; 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 * 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), * `shareView` (what the view follows, with its offset and last-known fix)
* or null when the code is refused. * and `worldTime` (null when the link was made live), or null when the
* code is refused.
*/ */
_parseShareCode(code) { _parseShareCode(code) {
const result = decodeShareView(code); const result = decodeShareView(code);
@@ -174,6 +178,7 @@ export class ShareLinkManager {
} }
const state = toLegacyShareState(result.state); const state = toLegacyShareState(result.state);
state.shareView = result.state.view; state.shareView = result.state.view;
state.worldTime = result.state.worldTime;
return state; return state;
} }
@@ -592,6 +597,10 @@ export class ShareLinkManager {
// that state in the share format. // that state in the share format.
const state = fromLegacyShareState(this._parseShareHash(params.toString())); const state = fromLegacyShareState(this._parseShareHash(params.toString()));
state.sharedAtMs = Math.floor(nowMs / 1000) * 1000; 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; let subject = null;
try { try {
subject = this._followSubjectProvider?.({ nowMs }) ?? null; subject = this._followSubjectProvider?.({ nowMs }) ?? null;
+25
View File
@@ -423,3 +423,28 @@ test('the layer coordinator replaces in-flight share layers with the navigated s
); );
coordinator.destroy(); 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();
});
+12
View File
@@ -4,6 +4,16 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
import { t, msg, formatDate } from '../i18n/index.js'; import { t, msg, formatDate } from '../i18n/index.js';
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js'; import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
import { UiLifetime } from './uiLifetime.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. */ /** How long a shared vessel may take to appear in the vessel feed. */
export const SHARED_VESSEL_WAIT_MS = 120_000; export const SHARED_VESSEL_WAIT_MS = 120_000;
@@ -146,6 +156,7 @@ export class ShareRestoration {
: Promise.resolve({ status: 'not-requested', share: null, layers: [] }); : Promise.resolve({ status: 'not-requested', share: null, layers: [] });
if (savedState) { if (savedState) {
this._hasShareState = true; this._hasShareState = true;
applySharedWorldTime(savedState);
// Reserve camera authority now; the delayed mesh-friendly flight may // Reserve camera authority now; the delayed mesh-friendly flight may
// run only if no newer user, voice, or tracking navigation has won. // run only if no newer user, voice, or tracking navigation has won.
this._initialShareNavigationGeneration = this._initialShareNavigationGeneration =
@@ -301,6 +312,7 @@ export class ShareRestoration {
layers: [], layers: [],
}); });
this._initialShareState = state; this._initialShareState = state;
applySharedWorldTime(state);
this._initialShareSelectionSuperseded = false; this._initialShareSelectionSuperseded = false;
this._hasShareState = true; this._hasShareState = true;
this._navigatedShareRestoreActive = true; this._navigatedShareRestoreActive = true;