mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
refactor: separate transit sources and request service
This commit is contained in:
1 parent
e284a9cd34
commit
3155e4e0bf
26 files changed
+1444
-1157
No files matched your search
@@ -8,9 +8,9 @@ import {
|
||||
} from 'gods-eye-view/sources/transit';
|
||||
|
||||
/** Mount the plugin and return a caller for its single route. */
|
||||
function install(plugin) {
|
||||
function install(plugin, mode = 'configureServer') {
|
||||
const routes = new Map();
|
||||
plugin.configureServer({
|
||||
plugin[mode]({
|
||||
middlewares: {
|
||||
use(route, handler) {
|
||||
routes.set(route, handler);
|
||||
@@ -22,7 +22,11 @@ function install(plugin) {
|
||||
const res = {
|
||||
headersSent: false,
|
||||
writeHead(status, headers) {
|
||||
Object.assign(this, { status, headers, headersSent: true });
|
||||
Object.assign(this, {
|
||||
status,
|
||||
headers: new Headers(headers),
|
||||
headersSent: true,
|
||||
});
|
||||
},
|
||||
end(body) {
|
||||
this.body = body;
|
||||
@@ -111,7 +115,7 @@ test('a 304 revalidation is a success, not a redirect with a missing Location',
|
||||
|
||||
const first = await call('/vehicles/mbta');
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(first.headers['X-GEV-Cache'], 'MISS');
|
||||
assert.equal(first.headers.get('X-GEV-Cache'), 'MISS');
|
||||
assert.equal(JSON.parse(first.body).count, 1);
|
||||
|
||||
// Past the freshness window the proxy asks again, conditionally.
|
||||
@@ -121,19 +125,19 @@ test('a 304 revalidation is a success, not a redirect with a missing Location',
|
||||
assert.equal(second.status, 200, 'an unchanged feed is not a failure');
|
||||
// MISS, not STALE-ERROR: the refresh SUCCEEDED. Treating 304 as a redirect
|
||||
// made this a failure that happened to be masked by the serve-stale path.
|
||||
assert.equal(second.headers['X-GEV-Cache'], 'MISS');
|
||||
assert.equal(second.headers.get('X-GEV-Cache'), 'MISS');
|
||||
assert.equal(JSON.parse(second.body).count, 1);
|
||||
|
||||
// And the third one too: a 304 must not have started a backoff ladder.
|
||||
t.mock.timers.setTime(Date.now() + TRANSIT_PROXY_TTL_MS + 1_000);
|
||||
const third = await call('/vehicles/mbta');
|
||||
assert.equal(third.status, 200);
|
||||
assert.equal(third.headers['X-GEV-Cache'], 'MISS');
|
||||
assert.equal(third.headers.get('X-GEV-Cache'), 'MISS');
|
||||
assert.equal(calls.length, 3, 'every poll past the TTL reached the operator');
|
||||
assert.equal(third.headers['Retry-After'], undefined);
|
||||
assert.equal(third.headers.get('Retry-After'), null);
|
||||
assert.equal(
|
||||
third.headers['X-Transit-Backoff'],
|
||||
undefined,
|
||||
third.headers.get('X-Transit-Backoff'),
|
||||
null,
|
||||
'no ladder was started',
|
||||
);
|
||||
});
|
||||
@@ -168,8 +172,8 @@ test('a differential feed condemns the snapshot that preceded it', async (t) =>
|
||||
200,
|
||||
`served ${after.status} with ${after.body}`,
|
||||
);
|
||||
assert.equal(after.headers['X-GEV-Cache'], 'NONE');
|
||||
assert.equal(after.headers['X-Transit-Backoff'], 'cooldown');
|
||||
assert.equal(after.headers.get('X-GEV-Cache'), 'NONE');
|
||||
assert.equal(after.headers.get('X-Transit-Backoff'), 'cooldown');
|
||||
});
|
||||
|
||||
test('a failing operator is put on a cooldown instead of being re-asked every poll', async (t) => {
|
||||
@@ -183,7 +187,7 @@ test('a failing operator is put on a cooldown instead of being re-asked every po
|
||||
const failed = await call('/vehicles/mbta');
|
||||
assert.equal(failed.status, 502);
|
||||
assert.equal(
|
||||
failed.headers['Retry-After'],
|
||||
failed.headers.get('Retry-After'),
|
||||
String(TRANSIT_BACKOFF_LADDER_MS[0] / 1000),
|
||||
);
|
||||
assert.equal(calls.length, 1);
|
||||
@@ -192,7 +196,7 @@ test('a failing operator is put on a cooldown instead of being re-asked every po
|
||||
t.mock.timers.setTime(Date.now() + 1_000);
|
||||
const held = await call('/vehicles/mbta');
|
||||
assert.equal(held.status, 503);
|
||||
assert.equal(held.headers['X-Transit-Backoff'], 'cooldown');
|
||||
assert.equal(held.headers.get('X-Transit-Backoff'), 'cooldown');
|
||||
assert.equal(calls.length, 1, 'the operator was left alone');
|
||||
|
||||
// Past the first rung it is tried again, and a second failure waits longer.
|
||||
@@ -200,7 +204,7 @@ test('a failing operator is put on a cooldown instead of being re-asked every po
|
||||
const retried = await call('/vehicles/mbta');
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(
|
||||
retried.headers['Retry-After'],
|
||||
retried.headers.get('Retry-After'),
|
||||
String(TRANSIT_BACKOFF_LADDER_MS[1] / 1000),
|
||||
);
|
||||
});
|
||||
@@ -280,7 +284,7 @@ test('a 304 reports that the operator answered, not that the body is new', async
|
||||
const call = install(transitProxy({ fetchImpl }));
|
||||
|
||||
const first = await call('/vehicles/mbta');
|
||||
assert.equal(Number(first.headers['X-Transit-Contact']), firstAt);
|
||||
assert.equal(Number(first.headers.get('X-Transit-Contact')), firstAt);
|
||||
assert.equal(JSON.parse(first.body).fetchedAt, firstAt);
|
||||
|
||||
t.mock.timers.setTime(Date.now() + TRANSIT_PROXY_TTL_MS + 1_000);
|
||||
@@ -292,7 +296,7 @@ test('a 304 reports that the operator answered, not that the body is new', async
|
||||
'the positions are as old as they are',
|
||||
);
|
||||
assert.equal(
|
||||
Number(second.headers['X-Transit-Contact']),
|
||||
Number(second.headers.get('X-Transit-Contact')),
|
||||
revalidatedAt,
|
||||
'but the operator answered just now',
|
||||
);
|
||||
@@ -300,8 +304,8 @@ test('a 304 reports that the operator answered, not that the body is new', async
|
||||
// Served from the fresh cache, the contact time is the last real contact —
|
||||
// not the moment this request happened to arrive.
|
||||
const third = await call('/vehicles/mbta');
|
||||
assert.equal(third.headers['X-GEV-Cache'], 'HIT');
|
||||
assert.equal(Number(third.headers['X-Transit-Contact']), revalidatedAt);
|
||||
assert.equal(third.headers.get('X-GEV-Cache'), 'HIT');
|
||||
assert.equal(Number(third.headers.get('X-Transit-Contact')), revalidatedAt);
|
||||
});
|
||||
|
||||
test('a feed served from cache during an outage does not claim fresh contact', async (t) => {
|
||||
@@ -319,9 +323,9 @@ test('a feed served from cache during an outage does not claim fresh contact', a
|
||||
|
||||
t.mock.timers.setTime(Date.now() + TRANSIT_PROXY_TTL_MS + 1_000);
|
||||
const during = await call('/vehicles/mbta');
|
||||
assert.equal(during.headers['X-GEV-Cache'], 'STALE-ERROR');
|
||||
assert.equal(during.headers.get('X-GEV-Cache'), 'STALE-ERROR');
|
||||
assert.equal(
|
||||
Number(during.headers['X-Transit-Contact']),
|
||||
Number(during.headers.get('X-Transit-Contact')),
|
||||
contactedAt,
|
||||
'silence does not count as an answer',
|
||||
);
|
||||
@@ -384,3 +388,44 @@ test('history read admission is separate from upstream admission and closes with
|
||||
assert.equal((await call('/vehicles/mbta')).status, 200);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test('development and preview mount the same transit service and close it on shutdown', async () => {
|
||||
for (const mode of ['configureServer', 'configurePreviewServer']) {
|
||||
let close;
|
||||
let handler;
|
||||
const plugin = transitProxy({
|
||||
fetchImpl: async () => {
|
||||
throw new Error('unexpected upstream');
|
||||
},
|
||||
});
|
||||
plugin[mode]({
|
||||
middlewares: {
|
||||
use(path, fn) {
|
||||
assert.equal(path, '/api/transit');
|
||||
handler = fn;
|
||||
},
|
||||
},
|
||||
httpServer: {
|
||||
once(event, fn) {
|
||||
assert.equal(event, 'close');
|
||||
close = fn;
|
||||
},
|
||||
},
|
||||
});
|
||||
async function call(method) {
|
||||
const res = {
|
||||
writeHead(status) {
|
||||
this.status = status;
|
||||
},
|
||||
end() {},
|
||||
};
|
||||
await handler({ url: '/feeds', method }, res);
|
||||
return res.status;
|
||||
}
|
||||
assert.equal(await call('GET'), 200);
|
||||
assert.equal(await call('TRACE'), 405);
|
||||
close();
|
||||
assert.equal(await call('GET'), 503);
|
||||
plugin.closeBundle();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createTransitService } from 'gods-eye-view/sources/transit-service';
|
||||
import { createTransitSource } from 'gods-eye-view/layers/transit/source';
|
||||
|
||||
const request = (path, method = 'GET') => ({
|
||||
url: `https://example.test${path}`,
|
||||
method,
|
||||
});
|
||||
|
||||
test('portable service confines requests to registered feeds and GET', async (t) => {
|
||||
let calls = 0;
|
||||
const service = createTransitService({
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
throw new Error('unexpected');
|
||||
},
|
||||
});
|
||||
t.after(service.close);
|
||||
for (const path of [
|
||||
'/other',
|
||||
'/api/transit/vehicles/unknown',
|
||||
'/api/transit/vehicles/https%3A%2F%2Fexample.test',
|
||||
]) {
|
||||
assert.equal((await service.handle(request(path))).status, 404);
|
||||
}
|
||||
for (const method of ['POST', 'HEAD', 'TRACE']) {
|
||||
assert.equal(
|
||||
(await service.handle(request('/api/transit/feeds', method))).status,
|
||||
405,
|
||||
);
|
||||
}
|
||||
const response = await service.handle(request('/api/transit/feeds'));
|
||||
assert.ok(response instanceof Response);
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok((await response.json()).feeds.some((feed) => feed.id === 'mbta'));
|
||||
assert.equal(calls, 0);
|
||||
service.close();
|
||||
assert.equal(
|
||||
(await service.handle(request('/api/transit/feeds'))).status,
|
||||
503,
|
||||
);
|
||||
});
|
||||
|
||||
test('source sends snapshots and bounded history through the supplied transport', async () => {
|
||||
const calls = [];
|
||||
const controller = new AbortController();
|
||||
const payload = {
|
||||
version: 1,
|
||||
feedId: 'mbta',
|
||||
vehicleId: 'bus 1',
|
||||
fixes: [],
|
||||
epochs: [],
|
||||
};
|
||||
const source = createTransitSource({
|
||||
fetchImpl: async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return Response.json(url.includes('/trail/') ? payload : { count: 0 });
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await (
|
||||
await source.requestSnapshot('mbta', { signal: controller.signal })
|
||||
).json(),
|
||||
{ count: 0 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
await source.getHistory('mbta', 'bus 1', { signal: controller.signal }),
|
||||
payload,
|
||||
);
|
||||
assert.deepEqual(
|
||||
calls.map(({ url }) => url),
|
||||
['/api/transit/vehicles/mbta', '/api/transit/trail/mbta/bus%201'],
|
||||
);
|
||||
assert.ok(calls.every(({ init }) => init.signal === controller.signal));
|
||||
controller.abort();
|
||||
assert.throws(
|
||||
() => source.requestSnapshot('mbta', { signal: controller.signal }),
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
assert.throws(
|
||||
() => source.getHistory('mbta', 'bus 1', { signal: controller.signal }),
|
||||
{ name: 'AbortError' },
|
||||
);
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test('source retains history validation and never retries through another transport', async () => {
|
||||
let calls = 0;
|
||||
const source = createTransitSource({
|
||||
fetchImpl: async () => {
|
||||
calls++;
|
||||
return Response.json({
|
||||
version: 1,
|
||||
feedId: 'wrong',
|
||||
vehicleId: 'bus',
|
||||
fixes: [],
|
||||
epochs: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
source.getHistory('mbta', 'bus'),
|
||||
/Invalid transit history response/,
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
await assert.rejects(
|
||||
source.getHistory('mbta', '../bus'),
|
||||
/Invalid transit vehicle identifier/,
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
Reference in new issue
Block a user