refactor: separate transit sources and request service

This commit is contained in:
Sameh Khamis committed 2026-09-15 07:41:56 -07:00
1 parent e284a9cd34
commit 3155e4e0bf
26 files changed
+1444 -1157

No files matched your search

+14 -2
View File
@@ -1,3 +1,4 @@
import { createTransitSource } from './source.js';
import { createTrails } from './trails.js';
import { createState } from './state.js';
import { createHeight } from './height.js';
@@ -23,15 +24,26 @@ import { TRANSIT_POLL_MS } from './policy.js';
* @param {{services: object}} options
* @returns {object} The data-layer module the manager registers.
*/
export function createTransitLayer({ services }) {
export function createTransitLayer({
services,
source = createTransitSource(),
}) {
if (!services?.overlays || !services?.render || !services?.sprites) {
throw new TypeError(
'A transit layer needs overlay, render and sprite services',
);
}
if (
typeof source?.requestSnapshot !== 'function' ||
typeof source?.getHistory !== 'function'
) {
throw new TypeError(
'A transit source needs snapshot and history operations',
);
}
const state = createState({ services });
const parts = {};
const context = { state, services, parts };
const context = { state, services, parts, source };
parts.trails = createTrails(context);
parts.height = createHeight(context);
parts.selection = createSelection(context);
+4 -8
View File
@@ -46,7 +46,7 @@ export const ORDER_REJECTS_BEFORE_RESET = 3;
* @param {object} context
* @returns {object}
*/
export function createIngestion({ state, services, parts }) {
export function createIngestion({ state, services, parts, source }) {
const { governorRequestRender } = services.render;
const { registerDynamicCredit, transitFeedCredit } = services.credits;
@@ -436,13 +436,9 @@ export function createIngestion({ state, services, parts }) {
status.loading = status.count === 0;
const promise = (async () => {
try {
const response = await fetch(
`/api/transit/vehicles/${encodeURIComponent(feed.id)}`,
{
signal: controller.signal,
headers: { Accept: 'application/json' },
},
);
const response = await source.requestSnapshot(feed.id, {
signal: controller.signal,
});
if (!response.ok) {
// The proxy says when it will next try the operator; carrying that
// through means the row can offer a time instead of just a shrug.
+34
View File
@@ -150,6 +150,7 @@ function groundStub(floorAt) {
function harness(
t,
{
source,
altitude = 4_000,
floorAt = () => 12,
at = BOSTON,
@@ -263,6 +264,7 @@ function harness(
/** Every call the floor cycle makes to the rendered-mesh sampler. */
const meshCalls = [];
const layer = createTransitLayer({
source,
services: {
ground,
mesh: {
@@ -3639,3 +3641,35 @@ test('moving and stationary transit stay adjacent above CCTV and Bikeshare durin
assert.equal(entry.marker.id, entry.key);
}
});
test('transit polls the supplied source and cancels it when disabled', async (t) => {
const calls = [];
const source = {
async requestSnapshot(feedId, { signal }) {
calls.push({ feedId, signal });
return Response.json(
snapshot(
feedId,
'MBTA',
[vehicle('injected', 42.36, -71.06, reported())],
{ fetchedAt: Date.now() },
),
);
},
async getHistory() {
throw new Error('No selected vehicle');
},
};
const app = harness(t, { source });
globalThis.fetch = () => {
throw new Error('Unexpected global fetch');
};
app.layer.enable(app.viewer);
await app.layer.update();
assert.ok(calls.some(({ feedId }) => feedId === 'mbta'));
assert.ok(app.vehicles().some((entry) => entry.record.id === 'injected'));
const before = calls.length;
app.layer.disable(app.viewer);
await app.layer.update();
assert.equal(calls.length, before);
});
+22
View File
@@ -0,0 +1,22 @@
import { fetchTransitHistory } from '../../sources/transitHistory.js';
/** Request transit snapshots through caller-owned transport. */
export function createTransitSource({
fetchImpl = (...args) => fetch(...args),
} = {}) {
return {
getHistory(feedId, vehicleId, { signal } = {}) {
signal?.throwIfAborted();
return fetchTransitHistory(feedId, vehicleId, signal, fetchImpl);
},
requestSnapshot(feedId, { signal } = {}) {
signal?.throwIfAborted();
if (typeof feedId !== 'string' || !feedId || feedId.length > 160)
throw new TypeError('A transit feed identifier is required');
return fetchImpl(`/api/transit/vehicles/${encodeURIComponent(feedId)}`, {
signal,
headers: { Accept: 'application/json' },
});
},
};
}
+5 -3
View File
@@ -1,4 +1,3 @@
import { fetchTransitHistory } from '../../data/transitProxy.js';
import { getRegisteredTransitFeed } from '../../data/transitFeeds.js';
import * as Cesium from 'cesium';
import {
@@ -23,7 +22,7 @@ export const TRAIL_VERTEX_LIMIT = 2048;
const STEP_M = 25;
/** Shared prepared paths. Floors are aligned to work with Google 3D tiles. */
export function createTrails({ state, services, parts }) {
export function createTrails({ state, services, parts, source }) {
let historyRequest = null;
let requestCount = 0,
abortCount = 0;
@@ -233,7 +232,10 @@ export function createTrails({ state, services, parts }) {
const controller = new AbortController();
historyRequest = controller;
requestCount++;
fetchTransitHistory(entry.feedId, entry.record.id, controller.signal)
source
.getHistory(entry.feedId, entry.record.id, {
signal: controller.signal,
})
.then((payload) => {
if (
controller.signal.aborted ||