mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #490 from bilawalsidhu/refactor/application-catalog
refactor: supply application catalogs explicitly
This commit is contained in:
16 files changed
+288
-50
No files matched your search
@@ -91,6 +91,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Let application data and controls receive the same explicit layer catalog; keep standalone defaults and registration-before-restoration ordering.
|
||||
|
||||
- Expose existing FIRMS CSV parsing and UTC time-window helpers through a portable package export, with shared contract fixtures.
|
||||
|
||||
- Separate map source factories from switching and resource ownership; retain current source IDs, attribution and fallbacks.
|
||||
|
||||
@@ -396,3 +396,16 @@ its engines. Its explicit module graph includes the smaller layer/UI groups;
|
||||
those independent groups retain their narrower gates. Source adapters and
|
||||
request services enter through construction, without replacing global fetch.
|
||||
The Node build group also owns the allowlisted static HTML template assembler.
|
||||
|
||||
## Application catalog
|
||||
|
||||
`gods-eye-view/application/catalog` captures caller-supplied layer instances and
|
||||
matching registration metadata. `application/data` registers that catalog, attaches
|
||||
coordinators after registration and seals it before controls start restoration.
|
||||
`application/controls` binds its layer services from the same catalog. The existing
|
||||
control surface and v2 sharing codec retain their established layer IDs; changing
|
||||
that schema requires a corresponding codec change.
|
||||
|
||||
`standalone/catalog` selects the existing page-scoped default instances and metadata.
|
||||
Reusable data setup imports no standalone layer defaults. The current compatibility
|
||||
source setters remain available while callers migrate to instance construction.
|
||||
@@ -1,5 +1,9 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
Application data setup receives an ordered layer catalog and serialization metadata.
|
||||
Controls use the same instances. The standalone composition selects the existing
|
||||
default catalog, and restoration starts only after all registrations are sealed.
|
||||
|
||||
The `sources/firms-csv` package export exposes existing FIRMS CSV parsing and
|
||||
UTC acquisition-window helpers without Node middleware or rendering dependencies.
|
||||
The standalone FIRMS provider, source defaults and parsing behavior are unchanged.
|
||||
|
||||
+3
-1
@@ -191,6 +191,8 @@
|
||||
"./context": "./src/data/contextStore.js",
|
||||
"./build/html": {
|
||||
"node": "./build/application-html.js"
|
||||
}
|
||||
},
|
||||
"./application/catalog": "./src/app/catalog.js",
|
||||
"./standalone/catalog": "./src/standalone/catalog.js"
|
||||
}
|
||||
}
|
||||
@@ -246,8 +246,8 @@ if [[ ! -f "$SOURCE_ROOT/src/data/cctv.js" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q "dataManager.register(cctvLayer)" "$SOURCE_ROOT/src/app/data.js"; then
|
||||
echo "error: CCTV layer not wired in src/app/data.js"
|
||||
if ! grep -q "^[[:space:]]*cctvLayer," "$SOURCE_ROOT/src/standalone/catalog.js"; then
|
||||
echo "error: CCTV layer not wired in src/standalone/catalog.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -555,5 +555,8 @@
|
||||
"src/app/startupChrome.js",
|
||||
"src/app/errors.js",
|
||||
"scripts/project-root.mjs",
|
||||
"src/toolProjectRoot.test.mjs"
|
||||
"src/toolProjectRoot.test.mjs",
|
||||
"src/app/catalog.js",
|
||||
"src/standalone/catalog.js",
|
||||
"src/app/catalog.test.mjs"
|
||||
]
|
||||
@@ -1043,7 +1043,8 @@
|
||||
"./standalone/settings",
|
||||
"./voice/actions",
|
||||
"./annotations",
|
||||
"./context"
|
||||
"./context",
|
||||
"./standalone/catalog"
|
||||
],
|
||||
"modules": [
|
||||
"src/annotations/annotationEngine.js",
|
||||
@@ -1500,7 +1501,9 @@
|
||||
"src/voice/realtimeController.js",
|
||||
"src/voice/voiceCost.js",
|
||||
"src/weatherEffectsMath.js",
|
||||
"src/worldFocus.js"
|
||||
"src/worldFocus.js",
|
||||
"src/app/catalog.js",
|
||||
"src/standalone/catalog.js"
|
||||
],
|
||||
"external": [
|
||||
"@mapbox/vector-tile",
|
||||
@@ -1510,5 +1513,10 @@
|
||||
"pbf",
|
||||
"satellite.js"
|
||||
]
|
||||
},
|
||||
"application-catalog": {
|
||||
"exports": ["./application/catalog"],
|
||||
"modules": ["src/app/catalog.js"],
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
const CONTROL_LAYER_IDS = Object.freeze({
|
||||
trafficLayer: 'traffic',
|
||||
flightsLayer: 'flights',
|
||||
militaryFlightsLayer: 'military',
|
||||
satellitesLayer: 'satellites',
|
||||
cctvLayer: 'cctv',
|
||||
radioLayer: 'radio',
|
||||
bikeshareLayer: 'bikeshare',
|
||||
aisLiveVesselsLayer: 'ais-live-vessels',
|
||||
militaryAwarenessLayer: 'military-awareness',
|
||||
militaryInstallationsLayer: 'military-installations',
|
||||
rocketLaunchesLayer: 'rocket-launches',
|
||||
});
|
||||
|
||||
/** Capture the ordered application instances and their serialization metadata. */
|
||||
export function createLayerCatalog(layers, metadata) {
|
||||
if (!Array.isArray(layers) || !Array.isArray(metadata))
|
||||
throw new TypeError(
|
||||
'Layer instances and serialization metadata are required',
|
||||
);
|
||||
const byId = new Map();
|
||||
for (const layer of layers) {
|
||||
if (typeof layer?.id !== 'string' || !layer.id || byId.has(layer.id))
|
||||
throw new TypeError(`Invalid or duplicate catalog layer: ${layer?.id}`);
|
||||
byId.set(layer.id, layer);
|
||||
}
|
||||
const ids = new Set();
|
||||
for (const entry of metadata) {
|
||||
if (!byId.has(entry?.id) || ids.has(entry.id))
|
||||
throw new TypeError(
|
||||
`Unmatched or duplicate catalog metadata: ${entry?.id}`,
|
||||
);
|
||||
ids.add(entry.id);
|
||||
}
|
||||
if (ids.size !== byId.size)
|
||||
throw new TypeError('Catalog metadata is incomplete');
|
||||
return Object.freeze({
|
||||
layers: Object.freeze([...layers]),
|
||||
metadata: Object.freeze(
|
||||
metadata.map((entry) => Object.freeze({ ...entry })),
|
||||
),
|
||||
get: (id) => byId.get(id),
|
||||
});
|
||||
}
|
||||
|
||||
/** Bind the current control surface to the exact instances registered by the app. */
|
||||
export function catalogControlServices(catalog) {
|
||||
if (!catalog?.get)
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
return Object.fromEntries(
|
||||
Object.entries(CONTROL_LAYER_IDS).map(([role, id]) => {
|
||||
const layer = catalog.get(id);
|
||||
if (!layer)
|
||||
throw new TypeError(`Control layer missing from catalog: ${id}`);
|
||||
return [role, layer];
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createLayerCatalog, catalogControlServices } from './catalog.js';
|
||||
import { createApplicationData } from './data.js';
|
||||
|
||||
const metadata = (layers) =>
|
||||
layers.map(({ id }) => ({ id, disposition: 'enabled-only' }));
|
||||
|
||||
test('catalog captures membership and metadata while preserving the supplied instances', () => {
|
||||
const first = { id: 'first' };
|
||||
const layers = [first];
|
||||
const schema = metadata(layers);
|
||||
const catalog = createLayerCatalog(layers, schema);
|
||||
layers.push({ id: 'later' });
|
||||
schema[0].disposition = 'invalid';
|
||||
assert.deepEqual(catalog.layers, [first]);
|
||||
assert.equal(catalog.get('first'), first);
|
||||
assert.equal(catalog.get('later'), undefined);
|
||||
assert.equal(catalog.metadata[0].disposition, 'enabled-only');
|
||||
assert.throws(() => catalog.layers.push({ id: 'later' }), TypeError);
|
||||
});
|
||||
|
||||
test('duplicate, missing and extra metadata fail before application registration', () => {
|
||||
const layers = [{ id: 'first' }];
|
||||
assert.throws(
|
||||
() => createLayerCatalog([...layers, ...layers], metadata(layers)),
|
||||
/duplicate/,
|
||||
);
|
||||
assert.throws(() => createLayerCatalog(layers, []), /incomplete/);
|
||||
assert.throws(
|
||||
() => createLayerCatalog(layers, metadata([{ id: 'other' }])),
|
||||
/Unmatched/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
createLayerCatalog(layers, [...metadata(layers), ...metadata(layers)]),
|
||||
/duplicate/,
|
||||
);
|
||||
});
|
||||
|
||||
test('data setup seals the caller catalog before controls can restore and drains its exact instances', async (t) => {
|
||||
const previous = globalThis.document;
|
||||
globalThis.document = { getElementById: () => null };
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete globalThis.document;
|
||||
else globalThis.document = previous;
|
||||
});
|
||||
const attached = [];
|
||||
const layers = [
|
||||
{
|
||||
id: 'one',
|
||||
attachDataManager: (manager) => attached.push([...manager.layers.keys()]),
|
||||
},
|
||||
{ id: 'two' },
|
||||
];
|
||||
const releases = [];
|
||||
let observed;
|
||||
const { dataManager } = createApplicationData({
|
||||
scene: { viewer: {} },
|
||||
controls: {
|
||||
styleManager: {
|
||||
attachDataManager(manager) {
|
||||
assert.equal(manager.registrationsFinalized, true);
|
||||
assert.equal(manager.layers.get('one').module, layers[0]);
|
||||
},
|
||||
},
|
||||
},
|
||||
catalog: createLayerCatalog(layers, metadata(layers)),
|
||||
defer: (release) => releases.push(release),
|
||||
onData: (manager) => {
|
||||
observed = manager;
|
||||
},
|
||||
});
|
||||
assert.equal(observed, dataManager);
|
||||
assert.deepEqual(attached, [['one', 'two']]);
|
||||
assert.deepEqual([...dataManager.layers.keys()], ['one', 'two']);
|
||||
assert.throws(() => dataManager.register({ id: 'late' }), /finalized/);
|
||||
for (const release of releases.reverse()) await release();
|
||||
assert.equal(dataManager.layers.size, 0);
|
||||
});
|
||||
|
||||
test('controls bind catalog instances rather than similarly named defaults', () => {
|
||||
const ids = [
|
||||
'traffic',
|
||||
'flights',
|
||||
'military',
|
||||
'satellites',
|
||||
'cctv',
|
||||
'radio',
|
||||
'bikeshare',
|
||||
'ais-live-vessels',
|
||||
'military-awareness',
|
||||
'military-installations',
|
||||
'rocket-launches',
|
||||
];
|
||||
const layers = ids.map((id) => ({ id }));
|
||||
const catalog = createLayerCatalog(layers, metadata(layers));
|
||||
const services = catalogControlServices(catalog);
|
||||
assert.equal(services.flightsLayer, layers[1]);
|
||||
assert.equal(services.aisLiveVesselsLayer, layers[7]);
|
||||
assert.equal(new Set(Object.values(services)).size, layers.length);
|
||||
assert.throws(
|
||||
() => catalogControlServices(createLayerCatalog([], [])),
|
||||
/Control layer missing/,
|
||||
);
|
||||
});
|
||||
Vendored
+3
-1
@@ -1,3 +1,4 @@
|
||||
import { catalogControlServices } from './catalog.js';
|
||||
import { StyleManager } from '../ui/composition.js';
|
||||
import { flyToAustin } from '../camera.js';
|
||||
import { initCockpitCloudEffects } from '../cockpitCloudEffects.js';
|
||||
@@ -8,12 +9,13 @@ export function createApplicationControls({
|
||||
loaderStatus,
|
||||
Controls = StyleManager,
|
||||
services,
|
||||
catalog,
|
||||
placeSearch,
|
||||
defer,
|
||||
}) {
|
||||
// Initialize the style manager (post-processing, HUD, locations, share links)
|
||||
const styleManager = new Controls(viewer, {
|
||||
services,
|
||||
services: { ...services, ...catalogControlServices(catalog) },
|
||||
mapStackController,
|
||||
placeSearch,
|
||||
});
|
||||
|
||||
+7
-36
@@ -1,24 +1,9 @@
|
||||
import { DataLayerManager } from '../data/manager.js';
|
||||
import flightsLayer from '../data/flights.js';
|
||||
import militaryFlightsLayer from '../data/militaryFlights.js';
|
||||
import alprCamerasLayer from '../data/alprCameras.js';
|
||||
import earthquakesLayer from '../data/earthquakes.js';
|
||||
import satellitesLayer from '../data/satellites.js';
|
||||
import rocketLaunchesLayer from '../data/rocketLaunches.js';
|
||||
import trafficLayer from '../data/traffic.js';
|
||||
import cctvLayer from '../data/cctv.js';
|
||||
import radioLayer from '../data/radio.js';
|
||||
import bikeshareLayer from '../data/bikeshare.js';
|
||||
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
|
||||
import militaryInstallationsLayer from '../data/militaryInstallations.js';
|
||||
import militaryAwarenessLayer from '../data/militaryAwareness.js';
|
||||
import localDataLayers from '../data/localLayers.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
|
||||
/** Register the application layer catalog before allowing state restoration. */
|
||||
export function createApplicationData({
|
||||
scene: { viewer },
|
||||
controls: { styleManager },
|
||||
catalog,
|
||||
allowQaRegistration,
|
||||
onData,
|
||||
defer,
|
||||
@@ -35,26 +20,12 @@ export function createApplicationData({
|
||||
);
|
||||
});
|
||||
onData?.(dataManager);
|
||||
dataManager.register(flightsLayer);
|
||||
dataManager.register(militaryFlightsLayer);
|
||||
dataManager.register(earthquakesLayer);
|
||||
dataManager.register(alprCamerasLayer);
|
||||
dataManager.register(satellitesLayer);
|
||||
dataManager.register(rocketLaunchesLayer);
|
||||
rocketLaunchesLayer.attachDataManager(dataManager);
|
||||
dataManager.register(trafficLayer);
|
||||
dataManager.register(cctvLayer);
|
||||
dataManager.register(radioLayer);
|
||||
dataManager.register(bikeshareLayer);
|
||||
dataManager.register(aisLiveVesselsLayer);
|
||||
dataManager.register(militaryInstallationsLayer);
|
||||
dataManager.register(militaryAwarenessLayer);
|
||||
militaryAwarenessLayer.attachDataManager(dataManager);
|
||||
for (const layer of localDataLayers) {
|
||||
dataManager.register(layer);
|
||||
}
|
||||
// Restoration starts only after the complete production registry is sealed.
|
||||
dataManager.finalizeRegistrations(LAYER_STATE_REGISTRY);
|
||||
if (!catalog?.layers || !catalog?.metadata)
|
||||
throw new TypeError('An application layer catalog is required');
|
||||
for (const layer of catalog.layers) dataManager.register(layer);
|
||||
for (const layer of catalog.layers) layer.attachDataManager?.(dataManager);
|
||||
// Restoration starts only after the caller's complete registry is sealed.
|
||||
dataManager.finalizeRegistrations(catalog.metadata);
|
||||
if (allowQaRegistration) {
|
||||
window.__gevQaRegisterLayer = (targetManager, layerModule) => {
|
||||
if (targetManager !== dataManager)
|
||||
|
||||
+11
-3
@@ -10,7 +10,7 @@ import { promisify } from 'node:util';
|
||||
const run = promisify(execFile);
|
||||
const bashTest = process.platform === 'win32' ? test.skip : test;
|
||||
|
||||
async function launch(overrides = {}, dotenv = '') {
|
||||
async function launch(overrides = {}, dotenv = '', omitCctv = false) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'gev-cctv-launch-'));
|
||||
try {
|
||||
await fs.mkdir(path.join(root, 'scripts'));
|
||||
@@ -20,8 +20,9 @@ async function launch(overrides = {}, dotenv = '') {
|
||||
for (const name of ['dev-cctv.sh', 'dev-fresh.sh', 'read-dotenv-value.mjs']) {
|
||||
await fs.copyFile(new URL(`../scripts/${name}`, import.meta.url), path.join(root, 'scripts', name));
|
||||
}
|
||||
await fs.mkdir(path.join(root, 'src', 'app'), { recursive: true });
|
||||
await fs.copyFile(new URL('./app/data.js', import.meta.url), path.join(root, 'src', 'app', 'data.js'));
|
||||
await fs.mkdir(path.join(root, 'src', 'standalone'), { recursive: true });
|
||||
const catalog = await fs.readFile(new URL('./standalone/catalog.js', import.meta.url), 'utf8');
|
||||
await fs.writeFile(path.join(root, 'src', 'standalone', 'catalog.js'), omitCctv ? catalog.replace(/^\s*cctvLayer,\s*$/m, '') : catalog);
|
||||
await fs.mkdir(path.join(root, 'node_modules'));
|
||||
await fs.symlink(fileURLToPath(new URL('.', import.meta.resolve('vite/package.json'))), path.join(root, 'node_modules', 'vite'), 'dir');
|
||||
await fs.writeFile(path.join(root, '.env'), dotenv);
|
||||
@@ -79,3 +80,10 @@ bashTest('CCTV preset shares dotenv precedence and names-only credential provena
|
||||
assert.equal(result.env.GEV_KEY_SETUP_EXTERNAL_KEYS, 'GOOGLE_MAPS_API_KEY');
|
||||
assert.doesNotMatch(result.output, /fixture-shell-maps|fixture-file-maps|fixture-file-voice/);
|
||||
});
|
||||
|
||||
bashTest('CCTV preset refuses a catalog that no longer registers its layer', async () => {
|
||||
await assert.rejects(launch({}, '', true), (error) => {
|
||||
assert.match(error.stdout, /CCTV layer not wired in src\/standalone\/catalog.js/);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createStandaloneCatalog } from './catalog.js';
|
||||
import { createStandalonePlaceSearch } from './placeSearch.js';
|
||||
import { createApplication } from '../app/application.js';
|
||||
import { createStandaloneScene } from './scene.js';
|
||||
@@ -22,6 +23,7 @@ export function createStandaloneApplication({
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
const loaderStatus = loadingScreen.querySelector('.loader-status');
|
||||
let placeSearch;
|
||||
const catalog = createStandaloneCatalog();
|
||||
return createApplication({
|
||||
createScene: (context) => {
|
||||
placeSearch = createStandalonePlaceSearch({
|
||||
@@ -37,9 +39,14 @@ export function createStandaloneApplication({
|
||||
});
|
||||
},
|
||||
createControls: (context) =>
|
||||
createStandaloneControls({ ...context, loaderStatus, placeSearch }),
|
||||
createStandaloneControls({
|
||||
...context,
|
||||
loaderStatus,
|
||||
placeSearch,
|
||||
catalog,
|
||||
}),
|
||||
createData: (context) =>
|
||||
createStandaloneData({ ...context, allowQaRegistration }),
|
||||
createStandaloneData({ ...context, allowQaRegistration, catalog }),
|
||||
createTools: (context) =>
|
||||
createStandaloneTools({ ...context, loadingScreen, placeSearch, voice }),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import flightsLayer from '../data/flights.js';
|
||||
import militaryFlightsLayer from '../data/militaryFlights.js';
|
||||
import alprCamerasLayer from '../data/alprCameras.js';
|
||||
import earthquakesLayer from '../data/earthquakes.js';
|
||||
import satellitesLayer from '../data/satellites.js';
|
||||
import rocketLaunchesLayer from '../data/rocketLaunches.js';
|
||||
import trafficLayer from '../data/traffic.js';
|
||||
import cctvLayer from '../data/cctv.js';
|
||||
import radioLayer from '../data/radio.js';
|
||||
import bikeshareLayer from '../data/bikeshare.js';
|
||||
import aisLiveVesselsLayer from '../data/aisLiveVessels.js';
|
||||
import militaryInstallationsLayer from '../data/militaryInstallations.js';
|
||||
import militaryAwarenessLayer from '../data/militaryAwareness.js';
|
||||
import localDataLayers from '../data/localLayers.js';
|
||||
import { LAYER_STATE_REGISTRY } from '../data/layerState.js';
|
||||
|
||||
import { createLayerCatalog } from '../app/catalog.js';
|
||||
|
||||
/** Select the existing standalone instances and current persistence schema. */
|
||||
export function createStandaloneCatalog() {
|
||||
return createLayerCatalog(
|
||||
[
|
||||
flightsLayer,
|
||||
militaryFlightsLayer,
|
||||
earthquakesLayer,
|
||||
alprCamerasLayer,
|
||||
satellitesLayer,
|
||||
rocketLaunchesLayer,
|
||||
trafficLayer,
|
||||
cctvLayer,
|
||||
radioLayer,
|
||||
bikeshareLayer,
|
||||
aisLiveVesselsLayer,
|
||||
militaryInstallationsLayer,
|
||||
militaryAwarenessLayer,
|
||||
...localDataLayers,
|
||||
],
|
||||
LAYER_STATE_REGISTRY,
|
||||
);
|
||||
}
|
||||
Vendored
+8
-1
@@ -1 +1,8 @@
|
||||
export { createApplicationControls as createStandaloneControls } from '../app/controls.js';
|
||||
import { createApplicationControls } from '../app/controls.js';
|
||||
import { createStandaloneCatalog } from './catalog.js';
|
||||
export function createStandaloneControls(options) {
|
||||
return createApplicationControls({
|
||||
catalog: createStandaloneCatalog(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -1 +1,8 @@
|
||||
export { createApplicationData as createStandaloneData } from '../app/data.js';
|
||||
import { createApplicationData } from '../app/data.js';
|
||||
import { createStandaloneCatalog } from './catalog.js';
|
||||
export function createStandaloneData(options) {
|
||||
return createApplicationData({
|
||||
catalog: createStandaloneCatalog(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user