Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 5 additions & 193 deletions .github/site-refactor.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion docs/site-architecture.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Site architecture plan

**Option 3 was decided by the owner on 2026-10-05: relocate queued loader functions to their owners.** The router starts at module evaluation exactly as today, and all loading uses the same single serialized queue chain. S2 changes only documentation and tools; it moves no application files. The projection proves connectivity, not runtime behavior or generated chunk bytes. This PR is ready for the owner to merge.
Expand Down Expand Up @@ -59,7 +59,6 @@
<!-- generated:changes -->
| Tier | Change / PR | Edit ids | Required code change |
| ---: | --- | --- | --- |
| 0 | Extract held address reader | add-file-navigation-href, add-import-navigation-history-navigation-href, retarget-application-world-resources-navigation-href | Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics. History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers. Only lower world caller uses navigationHref; other callers retain history behavior. |
| 0 | Relocate queued loader owners | add-file-scene-imports, add-file-world-imports, remove-queue-runtime, remove-shared-registry, remove-shared-world, retarget-adapter-scene-imports, retarget-router-world-imports, add-router-scene-imports, remove-directory-queue, retarget-boot-scene-imports, add-boot-world-imports | Decided by the owner: loader modules hold only import() calls through the existing queue. Own registry and packaged-runtime functions in scene; move the single last chain to lower browser/import-queue. Prove one queue chain, registration before the first scene call, the same entry-chunk modules and bytes, identical L1/L2/L3, L7 no worse and the real-iPad startup journey. Own the queued application-world import in world; no initializer inside the thunk. Move the runtime function out of the import-free browser queue implementation. Leave only importSceneRouter in startup/shared-imports. Move the world function to its owner. Adapter directly uses the same queued runtime function; preserve the frozen default adapter. Split router loader imports; preserve module-evaluation autostart. Import registry/runtime functions and register only the directory runtime loader using the existing object-directory import before first scene call. Directory accepts a typed runtime-loader registration; read the slot at scene invocation, not metadata construction. Missing registration fails explicitly. Body prestart and registry prestart reuse scene-owned promises. View prestart reuses the world-owned promise. |
<!-- /generated:changes -->

Expand Down
36 changes: 0 additions & 36 deletions docs/site-architecture/edits.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,5 @@
{
"edits": [
{
"op": "add-file",
"path": "site/model/navigation-href.mts",
"imports": [],
"change": "Extract held address reader",
"note": "Move navigationHref AND its owners WeakMap. Export a registration function for history; preserve deferred URL semantics.",
"id": "add-file-navigation-href"
},
{
"op": "add-import",
"from": "site/navigation/navigation-history.mts",
"to": "site/model/navigation-href.mts",
"kind": "value",
"change": "Extract held address reader",
"note": "History registers and unregisters its held address reader in the same WeakMap; re-export the getter for existing navigation callers.",
"id": "add-import-navigation-history-navigation-href"
},
{
"op": "retarget",
"from": "site/world/application-world-resources.mts",
"to": "site/navigation/navigation-history.mts",
"newTo": "site/model/navigation-href.mts",
"kind": "value",
"change": "Extract held address reader",
"note": "Only lower world caller uses navigationHref; other callers retain history behavior.",
"id": "retarget-application-world-resources-navigation-href",
"line": 18
},
{
"id": "add-file-scene-imports",
"op": "add-file",
Expand Down Expand Up @@ -155,14 +127,6 @@
}
],
"changes": [
{
"id": "extract-held-address-reader",
"edits": [
"add-file-navigation-href",
"add-import-navigation-history-navigation-href",
"retarget-application-world-resources-navigation-href"
]
},
{
"id": "relocate-queued-loader-owners",
"edits": [
Expand Down
2 changes: 1 addition & 1 deletion site/application-world-resources.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { loadCatalogueDots } from './world/dot-catalogues.mts';
import { annotationsForBodies, worldVisibilityPolicy } from './application-world-visibility.mts';
import { STELLAR_EXTENTS } from './stellar-extents.mts';
import { CONTEXT_DATASETS } from './world/context-datasets.mts';
import { navigationHref } from './navigation/navigation-history.mts';
import { navigationHref } from './model/navigation-href.mts';
import { onObjectEntry } from './directory/object-entries.mts';

/** The view an image mesh package is drawn in: the view of the dataset the mounted object shows of it (context-datasets.mts),
Expand Down
13 changes: 13 additions & 0 deletions site/model/navigation-href.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** The history owner of each window, so scene code reads the URL a deferred write will publish (`navigationHref`). */
const owners = new WeakMap<Window, { href(): string }>();

/** The page's URL as the app knows it: a history write deferred while the camera moves is already this URL. */
export function navigationHref(windowTarget: Window) {
return owners.get(windowTarget)?.href() ?? windowTarget.location.href;
}

/** Register a live address reader; disposal only removes this registration. */
export function registerNavigationHref(windowTarget: Window, owner: { href(): string }) {
owners.set(windowTarget, owner);
return () => { if (owners.get(windowTarget) === owner) owners.delete(windowTarget); };
}
34 changes: 34 additions & 0 deletions site/model/navigation-href.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import { navigationHref, registerNavigationHref } from './navigation-href.mts';

test('registration defers reads until the getter runs and disposal restores the browser address', () => {
const windowTarget = { location: { href: 'https://css.earth/earth/' } } as Window;
let href = 'https://css.earth/mars/', reads = 0;
const dispose = registerNavigationHref(windowTarget, { href() { reads++; return href; } });
assert.equal(reads, 0);
assert.equal(navigationHref(windowTarget), href);
href = 'https://css.earth/moon/';
assert.equal(navigationHref(windowTarget), href);
assert.equal(reads, 2);
dispose();
assert.equal(navigationHref(windowTarget), windowTarget.location.href);
assert.equal(reads, 2, 'disposed readers are never evaluated');
});

test('disposing a replaced registration preserves the current reader', () => {
const windowTarget = { location: { href: 'https://css.earth/earth/' } } as Window;
const disposeFirst = registerNavigationHref(windowTarget, { href: () => 'https://css.earth/mars/' });
const disposeSecond = registerNavigationHref(windowTarget, { href: () => 'https://css.earth/moon/' });
disposeFirst();
assert.equal(navigationHref(windowTarget), 'https://css.earth/moon/');
disposeSecond();
assert.equal(navigationHref(windowTarget), windowTarget.location.href);
});

test('world resources import the lower held-address reader without depending on history', () => {
const world = readFileSync(new URL('../application-world-resources.mts', import.meta.url), 'utf8');
assert.match(world, /import \{ navigationHref \} from '\.\/model\/navigation-href\.mts';/u);
assert.doesNotMatch(world, /from '\.\/navigation\/navigation-history\.mts'/u);
});
14 changes: 4 additions & 10 deletions site/navigation/navigation-history.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { BrowserWindow } from '../browser/browser-types.mts';
import { isRecord } from '@cssearth/core';
import { objectIdAtPath } from '../model/root-object.mts';
import type { NavigationHistory, NavigationIntent } from './navigation-types.mts';
import { navigationHref, registerNavigationHref } from '../model/navigation-href.mts';
export { navigationHref } from '../model/navigation-href.mts';
type Navigate = (id: string, intent: NavigationIntent) => unknown;
interface NavigationAnchor { href: string; target?: string; hasAttribute(name: string): boolean; getAttribute(name: string): string | null; }
function closestAnchor(target: EventTarget | null): NavigationAnchor | null {
Expand All @@ -13,14 +15,6 @@ function closestAnchor(target: EventTarget | null): NavigationAnchor | null {
const navigationId = (event: Event): unknown => 'detail' in event && isRecord(event.detail) ? event.detail.objectId : undefined;
const navigationFeature = (event: Event): string | undefined => 'detail' in event && isRecord(event.detail) && typeof event.detail.feature === 'string' && /^(?:city-)?[0-9]+$/u.test(event.detail.feature) ? event.detail.feature : undefined;

/** The history owner of each window, so scene code reads the URL a deferred write will publish (`navigationHref`). */
const owners = new WeakMap<Window, { href(): string }>();

/** The page's URL as the app knows it: a history write deferred while the camera moves is already this URL. */
export function navigationHref(windowTarget: Window) {
return owners.get(windowTarget)?.href() ?? windowTarget.location.href;
}

// On the iPad every URL change through the History API is followed by slow frames that are not our code: with no inspector
// attached, `replaceState` returned in 0 to 12 ms, yet the frame of the write took 29 to 42 ms and another of 26 to 34 ms
// came 0.2 s later (2026-10-04). Written a second after the camera rested, as they were, the two met the next gesture of
Expand Down Expand Up @@ -195,10 +189,10 @@ export function createNavigationHistory({ windowTarget, capture, navigate, navig
destroy() {
if (disposed) return;
flush(); disposed = true; listening.abort();
if (owners.get(windowTarget) === owner) owners.delete(windowTarget);
unregisterHref();
},
});
owners.set(windowTarget, owner);
const unregisterHref = registerNavigationHref(windowTarget, owner);
return owner;
}

Expand Down
29 changes: 29 additions & 0 deletions site/test/navigation-history.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import { sourceTest } from '@cssearth/objects/node/source-test';
const test = sourceTest();
import { createNavigationHistory, navigationHref } from '../navigation/navigation-history.mts';
import { navigationHref as heldHref } from '../model/navigation-href.mts';
import { ROOT_OBJECT_ID } from '../model/root-object.mts';

test('Back to the front page returns to the body it shows, not nowhere', () => {
Expand Down Expand Up @@ -231,3 +232,31 @@ test('a view kept for the entry is the one Back returns to after the next push',
listeners.get('popstate')!({ state: earth } as PopStateEvent);
assert.equal(calls.at(-1)?.[1].url, 'https://css.earth/earth/?v=near');
});

test('history registers its live held reader in the model and disposes it', () => {
assert.equal(navigationHref, heldHref, 'history re-exports the same getter');
const { windowTarget, motion } = readerWindow('https://css.earth/earth/');
const history = createNavigationHistory({ windowTarget, capture: () => '/earth/', navigate: () => {} });
motion(true);
history.commit('/mars/');
assert.equal(windowTarget.location.href, 'https://css.earth/earth/');
assert.equal(heldHref(windowTarget), 'https://css.earth/mars/');
history.commit('/moon/');
assert.equal(heldHref(windowTarget), 'https://css.earth/moon/', 'reads the latest held write');
history.destroy();
// After disposal, reads follow subsequent browser address changes.
windowTarget.history.replaceState(null, '', '/venus/');
assert.equal(heldHref(windowTarget), 'https://css.earth/venus/');
});

test('a superseded history that is destroyed leaves the live history reader registered', () => {
const { windowTarget, motion } = readerWindow('https://css.earth/earth/');
const first = createNavigationHistory({ windowTarget, capture: () => '/earth/', navigate: () => {} });
const second = createNavigationHistory({ windowTarget, capture: () => '/earth/', navigate: () => {} });
motion(true);
second.commit('/mars/');
assert.equal(heldHref(windowTarget), 'https://css.earth/mars/');
first.destroy();
assert.equal(heldHref(windowTarget), 'https://css.earth/mars/', 'only the registration a history still owns is removed');
second.destroy();
});
Loading