Browser compatibility (#17)

* Removed Proxy and local address code

* replaced node crypto with web crypto

* Replaced require with static imports

* removed commented out imports

* import db-hafas-stations on demand

* trying to handle undefined envs

* Less optimistic variable handling

* cleanup

* Small browser docs addition

* Linting

* No async in new Promise

* Bumped eslint to v9 and ecmaScript to 2025

* removed duplicated eslint config

* Bumped minimal node version to node 18

* Added node 24

* using math.random instead of webcrypto and reintroduced randomizeUserAgent

* Oh no node 24 is actually not released yet

* removed temp debug file
This commit is contained in:
McToel 2025-02-25 13:21:26 +01:00 committed by GitHub
parent 6d1d0c626f
commit 1aeb246622
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1480 additions and 1108 deletions

View file

@ -25,7 +25,6 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: node-version:
- 16.x
- 18.x - 18.x
- 20.x - 20.x
- 22.x - 22.x
@ -52,7 +51,6 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: node-version:
- 16.x
- 18.x - 18.x
- 20.x - 20.x
- 22.x - 22.x

View file

@ -1,6 +1,5 @@
import isObj from 'lodash/isObject.js'; import isObj from 'lodash/isObject.js';
import distance from 'gps-distance'; import distance from 'gps-distance';
import readStations from 'db-hafas-stations';
import {defaultProfile} from './lib/default-profile.js'; import {defaultProfile} from './lib/default-profile.js';
import {validateProfile} from './lib/validate-profile.js'; import {validateProfile} from './lib/validate-profile.js';
@ -28,21 +27,23 @@ const validateLocation = (loc, name = 'location') => {
}; };
const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => { const loadEnrichedStationData = (profile) => new Promise((resolve, reject) => {
const items = {}; import('db-hafas-stations').then(m => {
readStations.full() const items = {};
.on('data', (station) => { m.default.full()
items[station.id] = station; .on('data', (station) => {
items[station.name] = station; items[station.id] = station;
}) items[station.name] = station;
.once('end', () => { })
if (profile.DEBUG) { .once('end', () => {
console.log('Loaded station index.'); if (profile.DEBUG) {
} console.log('Loaded station index.');
resolve(items); }
}) resolve(items);
.once('error', (err) => { })
reject(err); .once('error', (err) => {
}); reject(err);
});
});
}); });
const applyEnrichedStationData = async (ctx, shouldLoadEnrichedStationData) => { const applyEnrichedStationData = async (ctx, shouldLoadEnrichedStationData) => {

View file

@ -37,7 +37,7 @@ import {formatTravellers} from '../format/travellers.js';
import {formatLoyaltyCard} from '../format/loyalty-cards.js'; import {formatLoyaltyCard} from '../format/loyalty-cards.js';
import {formatTransfers} from '../format/transfers.js'; import {formatTransfers} from '../format/transfers.js';
const DEBUG = (/(^|,)hafas-client(,|$)/).test(process.env.DEBUG || ''); const DEBUG = (/(^|,)hafas-client(,|$)/).test(typeof process !== 'undefined' ? process.env.DEBUG || '' : '');
const logRequest = DEBUG const logRequest = DEBUG
? (_, req, reqId) => console.error(String(req.body)) ? (_, req, reqId) => console.error(String(req.body))
: () => { }; : () => { };

View file

@ -1,49 +1,13 @@
import ProxyAgent from 'https-proxy-agent';
import {isIP} from 'net';
import {Agent as HttpsAgent} from 'https';
import roundRobin from '@derhuerst/round-robin-scheduler';
import {randomBytes} from 'crypto';
import {stringify} from 'qs'; import {stringify} from 'qs';
import {Request, fetch} from 'cross-fetch'; import {Request, fetch} from 'cross-fetch';
import {parse as parseContentType} from 'content-type'; import {parse as parseContentType} from 'content-type';
import {HafasError} from './errors.js'; import {HafasError} from './errors.js';
const proxyAddress = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || null; const randomBytesHexString = length => [...Array(length)].map(() => Math.floor(Math.random() * 16)
const localAddresses = process.env.LOCAL_ADDRESS || null; .toString(16))
.join('');
if (proxyAddress && localAddresses) { const id = randomBytesHexString(6)
console.error('Both env vars HTTPS_PROXY/HTTP_PROXY and LOCAL_ADDRESS are not supported.');
process.exit(1);
}
const plainAgent = new HttpsAgent({
keepAlive: true,
});
let getAgent = () => plainAgent;
if (proxyAddress) {
const agent = new ProxyAgent(proxyAddress, {
keepAlive: true,
keepAliveMsecs: 10 * 1000, // 10s
});
getAgent = () => agent;
} else if (localAddresses) {
const agents = process.env.LOCAL_ADDRESS.split(',')
.map((addr) => {
const family = isIP(addr);
if (family === 0) {
throw new Error('invalid local address:' + addr);
}
return new HttpsAgent({
localAddress: addr, family,
keepAlive: true,
});
});
const pool = roundRobin(agents);
getAgent = () => pool.get();
}
const id = randomBytes(3)
.toString('hex'); .toString('hex');
const randomizeUserAgent = (userAgent) => { const randomizeUserAgent = (userAgent) => {
let ua = userAgent; let ua = userAgent;
@ -101,8 +65,8 @@ const request = async (ctx, userAgent, reqData) => {
delete reqData.endpoint; delete reqData.endpoint;
const rawReqBody = profile.transformReqBody(ctx, reqData.body); const rawReqBody = profile.transformReqBody(ctx, reqData.body);
const req = profile.transformReq(ctx, { const reqOptions = profile.transformReq(ctx, {
agent: getAgent(), keepalive: true,
method: reqData.method, method: reqData.method,
// todo: CORS? referrer policy? // todo: CORS? referrer policy?
body: JSON.stringify(rawReqBody), body: JSON.stringify(rawReqBody),
@ -114,7 +78,6 @@ const request = async (ctx, userAgent, reqData) => {
'user-agent': profile.randomizeUserAgent 'user-agent': profile.randomizeUserAgent
? randomizeUserAgent(userAgent) ? randomizeUserAgent(userAgent)
: userAgent, : userAgent,
'connection': 'keep-alive', // prevent excessive re-connecting
...reqData.headers, ...reqData.headers,
}, },
redirect: 'follow', redirect: 'follow',
@ -122,15 +85,14 @@ const request = async (ctx, userAgent, reqData) => {
}); });
let url = endpoint + (reqData.path || ''); let url = endpoint + (reqData.path || '');
if (req.query) { if (reqOptions.query) {
url += '?' + stringify(req.query, {arrayFormat: 'brackets', encodeValuesOnly: true}); url += '?' + stringify(reqOptions.query, {arrayFormat: 'brackets', encodeValuesOnly: true});
} }
const reqId = randomBytes(3) const reqId = randomBytesHexString(6);
.toString('hex'); const fetchReq = new Request(url, reqOptions);
const fetchReq = new Request(url, req);
profile.logRequest(ctx, fetchReq, reqId); profile.logRequest(ctx, fetchReq, reqId);
const res = await fetch(url, req); const res = await fetch(url, reqOptions);
const errProps = { const errProps = {
// todo [breaking]: assign as non-enumerable property // todo [breaking]: assign as non-enumerable property
@ -150,7 +112,7 @@ const request = async (ctx, userAgent, reqData) => {
let cType = res.headers.get('content-type'); let cType = res.headers.get('content-type');
if (cType) { if (cType) {
const {type} = parseContentType(cType); const {type} = parseContentType(cType);
if (type !== req.headers['Accept']) { if (type !== reqOptions.headers['Accept']) {
throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps); throw new HafasError('invalid/unsupported response content-type: ' + cType, null, errProps);
} }
} }

View file

@ -1,7 +1,5 @@
import {createRequire} from 'module'; import dbnavBase from '../dbnav/base.json' with { type: 'json' };
const require = createRequire(import.meta.url); import dbregioguideBase from '../dbregioguide/base.json' with { type: 'json' };
const dbnavBase = require('../dbnav/base.json');
const dbregioguideBase = require('../dbregioguide/base.json');
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
// journeys() // journeys()

View file

@ -1,7 +1,4 @@
import {createRequire} from 'module'; import baseProfile from './base.json' with { type: 'json' };
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js'; import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatTripReq} from './trip-req.js'; import {formatTripReq} from './trip-req.js';

View file

@ -1,6 +1,4 @@
import {createRequire} from 'module'; import baseProfile from './base.json' with { type: 'json' };
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
import {formatTripReq} from './trip-req.js'; import {formatTripReq} from './trip-req.js';

View file

@ -1,7 +1,4 @@
import {createRequire} from 'module'; import baseProfile from './base.json' with { type: 'json' };
const require = createRequire(import.meta.url);
const baseProfile = require('./base.json');
import {products} from '../../lib/products.js'; import {products} from '../../lib/products.js';
import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js'; import {formatJourneysReq, formatRefreshJourneyReq} from './journeys-req.js';
import {formatLocationFilter} from './location-filter.js'; import {formatLocationFilter} from './location-filter.js';

2407
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -53,10 +53,9 @@
], ],
"packageManager": "npm@9.2.0", "packageManager": "npm@9.2.0",
"engines": { "engines": {
"node": ">=16" "node": ">=18"
}, },
"dependencies": { "dependencies": {
"@derhuerst/round-robin-scheduler": "^1.0.4",
"content-type": "^1.0.4", "content-type": "^1.0.4",
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"db-hafas-stations": "^1.0.0", "db-hafas-stations": "^1.0.0",
@ -69,12 +68,15 @@
"uuid": "^11.0.5" "uuid": "^11.0.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.20.0",
"@pollyjs/adapter-node-http": "^6.0.5", "@pollyjs/adapter-node-http": "^6.0.5",
"@pollyjs/core": "^6.0.5", "@pollyjs/core": "^6.0.5",
"@pollyjs/persister-fs": "^6.0.5", "@pollyjs/persister-fs": "^6.0.5",
"@stylistic/eslint-plugin": "^3.1.0", "@stylistic/eslint-plugin": "^3.1.0",
"db-rest": "github:derhuerst/db-rest", "db-rest": "github:derhuerst/db-rest",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"globals": "^15.15.0",
"hafas-rest-api": "^5.1.3", "hafas-rest-api": "^5.1.3",
"is-coordinates": "^2.0.2", "is-coordinates": "^2.0.2",
"is-roughly-equal": "^0.1.0", "is-roughly-equal": "^0.1.0",

View file

@ -76,6 +76,12 @@ There are [community-maintained TypeScript typings available as `@types/hafas-cl
> [!IMPORTANT] > [!IMPORTANT]
> Depending on your use case, it is very important that you employ caching, either with a simple [HTTP proxy cache](https://github.com/traines-source/time-space-train-planner/blob/master/deployments/nginx-cache.conf) in front of the REST API or by using [cached-hafas-client](https://github.com/public-transport/cached-hafas-client) (where, of course, you can just drop in a `db-vendo-client` instead of a `hafas-client` instance). Also see [db-rest](https://github.com/derhuerst/db-rest), which does this and some more plumbing. > Depending on your use case, it is very important that you employ caching, either with a simple [HTTP proxy cache](https://github.com/traines-source/time-space-train-planner/blob/master/deployments/nginx-cache.conf) in front of the REST API or by using [cached-hafas-client](https://github.com/public-transport/cached-hafas-client) (where, of course, you can just drop in a `db-vendo-client` instead of a `hafas-client` instance). Also see [db-rest](https://github.com/derhuerst/db-rest), which does this and some more plumbing.
## Browser usage
`db-vendo-client` is mostly browser compatible, however none of the endpoints enables CORS, so it is impossible to use `db-vendo-client` in normal browser environments. It was tested with vite + capacitorjs and should also work with cordova or react native and similar projects.
**Limitations:** Does not work with `enrichStations` option enabled.
## Related Projects ## Related Projects
- [hafas-client](https://github.com/public-transport/hafas-client/) including further related projects - [hafas-client](https://github.com/public-transport/hafas-client/) including further related projects

View file

@ -1,13 +1,11 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js'; import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-departures.json'); import res from './fixtures/dbnav-departures.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js'; import {dbnavDepartures as expected} from './fixtures/dbnav-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,11 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js'; import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-refresh-journey.json'); import res from './fixtures/dbnav-refresh-journey.json' with { type: 'json' };
import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js'; import {dbNavJourney as expected} from './fixtures/dbnav-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,11 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js'; import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-stop.json'); import res from './fixtures/dbnav-stop.json' with { type: 'json' };
import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js'; import {dbnavDepartures as expected} from './fixtures/dbnav-stop.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbnav/index.js'; import {profile as rawProfile} from '../p/dbnav/index.js';
const res = require('./fixtures/dbnav-trip.json'); import res from './fixtures/dbnav-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbnav-trip.js'; import {dbTrip as expected} from './fixtures/dbnav-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbregioguide/index.js'; import {profile as rawProfile} from '../p/dbregioguide/index.js';
const res = require('./fixtures/dbregioguide-trip.json'); import res from './fixtures/dbregioguide-trip.json' with { type: 'json' };
import {dbTrip as expected} from './fixtures/dbregioguide-trip.js'; import {dbTrip as expected} from './fixtures/dbregioguide-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js'; import {profile as rawProfile} from '../p/dbweb/index.js';
const res = require('./fixtures/dbris-arrivals.json'); import res from './fixtures/dbris-arrivals.json' with { type: 'json' };
import {dbArrivals as expected} from './fixtures/dbris-arrivals.js'; import {dbArrivals as expected} from './fixtures/dbris-arrivals.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js'; import {profile as rawProfile} from '../p/dbweb/index.js';
const res = require('./fixtures/dbweb-departures.json'); import res from './fixtures/dbweb-departures.json' with { type: 'json' };
import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js'; import {dbwebDepartures as expected} from './fixtures/dbweb-departures.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js'; import {profile as rawProfile} from '../p/dbweb/index.js';
const res = require('./fixtures/dbweb-journey.json'); import res from './fixtures/dbweb-journey.json' with { type: 'json' };
import {dbwebJourney as expected} from './fixtures/dbweb-journey.js'; import {dbwebJourney as expected} from './fixtures/dbweb-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js'; import {profile as rawProfile} from '../p/dbweb/index.js';
const res = require('./fixtures/dbweb-refresh-journey.json'); import res from './fixtures/dbweb-refresh-journey.json' with { type: 'json' };
import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js'; import {dbJourney as expected} from './fixtures/dbweb-refresh-journey.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,13 +1,10 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import {createClient} from '../index.js'; import {createClient} from '../index.js';
import {profile as rawProfile} from '../p/dbweb/index.js'; import {profile as rawProfile} from '../p/dbweb/index.js';
const res = require('./fixtures/dbweb-trip.json'); import res from './fixtures/dbweb-trip.json' with { type: 'json' };
import {dbwebTrip as expected} from './fixtures/dbweb-trip.js'; import {dbwebTrip as expected} from './fixtures/dbweb-trip.js';
const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false}); const client = createClient(rawProfile, 'public-transport/hafas-client:test', {enrichStations: false});

View file

@ -1,8 +1,5 @@
// todo: use import assertions once they're supported by Node.js & ESLint // todo: use import assertions once they're supported by Node.js & ESLint
// https://github.com/tc39/proposal-import-assertions // https://github.com/tc39/proposal-import-assertions
import {createRequire} from 'module';
const require = createRequire(import.meta.url);
import tap from 'tap'; import tap from 'tap';
import { import {
checkIfResponseIsOk as checkIfResIsOk, checkIfResponseIsOk as checkIfResIsOk,