npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vdv-453-client

v2.1.0

Published

Subscribe to realtime public transport data from a VDV-453/VDV-454 API.

Downloads

115

Readme

vdv-453-client

A JavaScript client for realtime public transport data systems following the VDV-453 v2.3.2b/VDV-454 v1.2.2 specs (from 2013). Such systems are widespread in Germany, being the realtime data backends (Datendrehscheiben) of many regional transit authorities/associations.

npm version ISC-licensed minimum Node.js version chat with me on Twitter

vdv-453-client is a library only, intended to be embedded into other tools (e.g. vdv-453-nats-adapter). It subscribes to services (see below), fetches the XML data, converts it to JSON, and emits it via an event.

[!NOTE] This client supports neither the latest 2.x spec versions (VDV-453 v2.6.1/VDV-454 v2.2.1) nor the latest 3.x spec versions (VDV-453 v3.0/VDV-454 v3.0). Refer to the tracking Issue #2.

The VDV-453 spec defines the basic protocol that client (usually the data consumer) and server (usually the provider) use to communicate; It uses HTTP POST requests with XML bodies. VDV-453 also defines some (domain-specific) services on top, e.g. DFI for fetching departures at stops/stations. A client subscribes to such services, optionally with service-specific parameters, e.g. filters to reduce the number of subscribed items.

On top of VDV-453, VDV-454 defines two additional services: REF-AUS for the exchange of daily schedule data, and AUS for realtime data like prognosed delays & cancellations.

vdv-453-client has been written specifically for VBB's Datendrehscheibe. However, we're open to changes that make vdv-453-client compatible with other VDV-453/-454 systems.

Installation

npm install vdv-453-client

Getting Started

[!IMPORTANT] While vdv-453-client is used in a production system at VBB, it hasn't been tested with other VDV-453/-454 systems. Also, there's tracking issue #1 regarding automated tests.

Leitstellenkennung

With the organisation providing the VDV 453 API, you will have to agree upon your client's Leitstellenkennung, which – a bit like an HTTP User-Agent – allows the server to identify your client:

6.1.3 Leitstellenkennung

Um Botschaften verschiedener Kommunikationspartner innerhalb eines Dienstes unterscheiden zu können, enthält jede Nachricht eine eindeutige Leitstellenkennung (Attribut Sender) des nachfragenden Systems. […]

const LEITSTELLE = 'MY_VDV_CLIENT'

You will also have to configure their Leitstellenkennung, which they use for calls to your client.

const THEIR_LEITSTELLE = 'SOME_VDV_API'

server address

We configure the server's address. It needs to be the HTTP(S) base URL without your Leitstellenkennung.

const ENDPOINT = 'http://vdv-api.example.org/'

local HTTP server

[!NOTE] The VDV-453 spec expects the client (consumer) to listen for HTTP requests from the server (provider), in order to allow the server to notify the client when new data is available, sort of like a webhook. This means that your client's machine will have to have an open TCP port! Once you have chosen your client's port, it needs to be configured on the server side.

5.1.3.1 Datenbereitstellung signalisieren (DatenBereitAnfrage)

Ist das Abonnement eingerichtet und sind die Daten bereitgestellt, wird der Datenkonsument durch eine DatenBereitAnfrage über das Vorhandensein aktualisierter Daten informiert.

5.1.8 Alive-Handling

Die Statusabfrage dient dem Feststellen der Verfügbarkeit von Diensten. Dazu werden zwei spezielle Informationskanäle verwendet (Ziel-URL status.xml, clientstatus.xml), die jeder Dienst bereitstellen muss.

configuring the client

import {createClient as createVdv453Client} from 'vdv-453-client'

const {
	httpServer,
	data,
	dfiSubscribe,
	dfiUnsubscribe,
	ausSubscribe,
	ausUnsubscribe,
} = createVdv453Client({
	leitstelle: LEITSTELLE,
	theirLeitstelle: THEIR_LEITSTELLE,
	endpoint: ENDPOINT,
})

// createClient() returns an HTTP server, which you still need to call listen() on.
await new Promise((resolve, reject) => {
	httpServer.listen(3000, (err) => {
		if (err) reject(err)
		else resolve()
	})
})

const unsubscribeTasks = []
process.once('SIGINT', {
	Promise.all(unsubscribeTasks.map(task => task()))
	.then(() => {
		httpServer.close()
	})
	.catch((err) => {
		console.error(err)
		process.exit(1)
	})
})

subscribing to services

By design, every VDV-453/-454 subscription must have a TTL – in other words: a pre-set point in time at which it expires.

5.1.1 Abonnement-Verfahren – Überblick

[…]

Abonnements besitzen eine vom Client definierte Lebensspanne und werden nach Ablauf automatisch vom Server gelöscht. […]

5.1.2.1 Abonnementsanfrage (AboAnfrage)

[…]

Allen Abonnements aller Dienste wird beim Einrichten ein Verfallszeitstempel (VerfallZst) durch den Client mitgegeben. […]

Therefore, when subscribing to a service, the client must provide an expiration date+time. Use opt.expiresAt to provide a different TTL than vdv-453-client's default of 1 hour.

// subscribe to VDV-453 DFI service
// the ID of the stop/station (a.k.a. "Anzeigerbereich") depends on your region's data
const SOME_ANZEIGERBEREICH_ID = 'my stop ID'
const {aboId: dfiAboId} = await dfiSubscribe(SOME_ANZEIGERBEREICH_ID)
unsubscribeTasks.push(() => dfiUnsubscribe(dfiAboId))
data.on('dfi:AZBNachricht', (azbNachricht) => {
	console.log(azbNachricht)
})

// subscribe to VDV-454 AUS service
const {aboId: ausAboId} = await ausSubscribe({
	expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes from now
})
unsubscribeTasks.push(() => ausUnsubscribe(ausAboId))
data.on('aus:IstFahrt', (istFahrt) => {
	console.log(istFahrt)
})

[!WARNING] Currently, vdv-453-client has some shortcomings in the handling of subscriptions; For example, it does not persist the information about its subscriptions, and it does not respond to the server with its active subscriptions (AktiveAbos) when asked. Refer to the tracking Issue #3 for more details.

API

[!TIP] The REF-AUS & AUS services are defined in VDV-454. All other services are defined in VDV-453.

A client instance can be created by calling createClient(). This object is referred to as client below.

createClient() takes the following arguments:

  1. cfg: an object with these fields:
    • leitstelle: the client's Leitstellenkennung (see the "Getting Started" section)
    • theirLeitstelle: the server's Leitstellenkennung
  2. opt: an optional object whose fields override these defaults:
    • logger: used for general log messages; must be pino-compatible
    • requestsLogger: used for logging HTTP requests/responses; must be pino-compatible
    • fetchSubscriptionsDataPeriodically: if subscriptions' data should be fetched manually periodically, regardless of wether the server proactively reports new data using DatenBereitAnfrages – default: true
    • on*(): hooks for debug logging, keeping metrics, etc. (see the "Hooks" section)

client.httpServer

The client HTTP server (see section above), an http.Server that you still need to call listen() on.

client.data

An EventEmitter that will emit ${service}:${rootSubTag}, where service & rootSubTag depend on the service that you subscribe on.

[!NOTE] The arguments of the events below, unless otherwise noted, are JSON equivalents of the XML trees sent by the server (see the XML to JSON mapping section).

event aus:IstFahrt

Arguments:

  1. istFahrt – The AUS IstFahrt.

event raw:dfi:AZBNachricht

Arguments:

  1. azbNachricht – The whole DFI AZBNachricht, usually containing many AZBFahrplanlages, AZBFahrtLoeschens, etc.

event raw:aus:AUSNachricht

Arguments:

  1. ausNachricht – The whole AUS AUSNachricht, usually containing many IstFahrts.

client.dfiSubscribe()

dfiSubscribe(azbId, opt = {}) is an async function that takes the following arguments:

  1. azbId: The ID of a DFI Anzeigerbereich, a.k.a. a stop or station.
  2. opt (optional): An object whose fields override the following defaults:
    • expiresAt: Date.now() + DFI_DEFAULT_SUBSCRIPTION_TTL,
    • linienId: null,
    • richtungsId: null,
    • vorschauzeit: 10 (in minutes)
    • hysterese: 1 (in seconds)
    • fetchInterval: 30_000 (in milliseconds)

After subscribing successfully, it will return an object with the following fields:

  • aboId: The ID that represents the subscription. It can be used to unsubscribe.

client.dfiFetchData()

dfiFetchData(opt = {}) is an async function that takes the following arguments:

  1. opt (optional): An object whose fields override the following defaults:
    • abortController: inactive AbortController – Pass in your own to be able to .abort() the fetching.

client.dfiUnsubscribe()

dfiUnsubscribe(...aboIds) is an async function that takes the following arguments:

  1. aboIds: >0 subscription IDs.

client.dfiUnsubscribeAll()

dfiUnsubscribeAll() is an async function that 0 arguments. It unsubscribes from all active DFI subscriptions the server knows about.

client.dfiCheckServerStatus()

Sends a VDV-453 StatusAnfrage to the server, to obtain information about the server's state related to the DFI service.

dfiCheckServerStatus() is an async function that returns an object with the following fields:

  • datenBereit (boolean): If the server has new DFI data to be fetched by the client.
  • startDienstZst (ISO 8601 string or null): When the server's DFI service has been started.
  • statusAntwort (object): The whole response's StatusAntwort element.

client.ausSubscribe()

ausSubscribe(opt = {}) is an async function that takes the following arguments:

  1. opt (optional): An object whose fields override the following defaults:
    • expiresAt: Date.now() + AUS_DEFAULT_SUBSCRIPTION_TTL,
    • vorschauzeit: 10 (in minutes)
    • hysterese: 60 (in seconds)
    • fetchInterval: 30_000 (in milliseconds)

After subscribing successfully, it will return an object with the following fields:

  • aboId: The ID that represents the subscription. It can be used to unsubscribe.

client.ausFetchData()

Works like client.dfiFetchData(), except for AUS.

client.ausUnsubscribe()

Works like client.dfiUnsubscribe(), except for AUS.

client.ausUnsubscribeAll()

Works like client.dfiUnsubscribeAll(), except for AUS.

client.ausCheckServerStatus()

Works like client.dfiCheckServerStatus(), except for AUS.

client.unsubscribeAllOwned()

An async function that will unsubscribe from all (unexpired) subscriptions created using client.

error handling

The functions in client may reject with the following errors:

  • Vdv453HttpError – The server has rejected the client's HTTP request, e.g. because it is malformed, or because the server is overloaded.
  • Vdv453ApiError – The server has accepted the client's HTTP request but signaled that it couldn't process it, e.g. because a subscription filter is not valid.
  • Error – A generic error thrown in some cases.

XML to JSON mapping

vdv-453-client uses xml-stream-saxes to parse XML into JavaScript/JSON trees.

For example, the following (simplified) XML IstFahrt:

<IstFahrt Zst="2024-04-11T09:10:11Z">
	<LinienID>M8</LinienID>
	<Komplettfahrt>false</Komplettfahrt>
	<IstHalt>
		<HaltID>ODEG_900170006</HaltID>
		<Abfahrtszeit>2024-04-11T11:52:00Z</Abfahrtszeit>
	</IstHalt>
	<IstHalt>
		<HaltID>ODEG_900171517</HaltID>
		<Abfahrtszeit>2024-04-11T12:07:00Z</Abfahrtszeit>
	</IstHalt>
</IstFahrt>

into the following JSON tree.

{
	'$name': 'IstFahrt',
	'$': {
		Zst: '2024-04-11T09:10:11Z',
	},
	'$children': [
		{
			'$name': 'LinienID',
			'$text': 'M8',
			'$children': ['M8'],
		},
		{
			'$name': 'Komplettfahrt',
			'$text': 'false',
			'$children': ['false'],
		},
		{
			'$name': 'IstHalt',
			'$children': [
				{
					'$name': 'HaltID',
					'$text': 'ODEG_900170006',
					'$children': ['ODEG_900170006'],
				},
				{
					'$name': 'Abfahrtszeit',
					'$text': '2024-04-11T11:52:00Z',
					'$children': ['2024-04-11T11:52:00Z'],
				}
			],
			HaltID: {
				'$name': 'HaltID',
				'$text': 'ODEG_900170006',
				'$children': ['ODEG_900170006'],
			},
			Abfahrtszeit: {
				'$name': 'Abfahrtszeit',
				'$text': '2024-04-11T11:52:00Z',
				'$children': ['2024-04-11T11:52:00Z'],
			},
		},
		{
			'$name': 'IstHalt',
			'$children': [
				{
					'$name': 'HaltID',
					'$text': 'ODEG_900171517',
					'$children': ['ODEG_900171517'],
				},
				{
					'$name': 'Abfahrtszeit',
					'$text': '2024-04-11T12:07:00Z',
					'$children': ['2024-04-11T12:07:00Z'],
				}
			],
			HaltID: {
				'$name': 'HaltID',
				'$text': 'ODEG_900171517',
				'$children': ['ODEG_900171517'],
			},
			Abfahrtszeit: {
				'$name': 'Abfahrtszeit',
				'$text': '2024-04-11T12:07:00Z',
				'$children': ['2024-04-11T12:07:00Z'],
			},
		}
	],
	LinienID: {
		'$name': 'LinienID',
		'$text': 'M8',
		'$children': ['M8'],
	},
	Komplettfahrt: {
		'$name': 'Komplettfahrt',
		'$text': 'false',
		'$children': ['false'],
	},
	IstHalt: {
		'$name': 'IstHalt',,
		'$children': [
			{
				'$name': 'HaltID',
				'$text': 'ODEG_900171517',
				'$children': ['ODEG_900171517'],
			},
			{
				'$name': 'Abfahrtszeit',
				'$text': '2024-04-11T12:07:00Z',
				'$children': ['2024-04-11T12:07:00Z'],
			}
		],
		HaltID: {
			'$name': 'HaltID',
			'$text': 'ODEG_900171517',
			'$children': ['ODEG_900171517'],
		},
		Abfahrtszeit: {
			'$name': 'Abfahrtszeit',
			'$text': '2024-04-11T12:07:00Z',
			'$children': ['2024-04-11T12:07:00Z'],
		},
	},
}

[!WARNING] Among all children of a node, the last of each kind ($name) will also be exposed on the node as node[child.$name] (e.g. LinienID above). Because you usually can't predict the number of children in a node for sure (nor the order), we recommend to always iterate over $children and only use the "direct" named properties if you know what you're doing.

hooks

createClient()'s opt object allows you to define the following hooks. A hook can be a sync or an async function.

![IMPORTANT] Your hook functions must never throw/reject, otherwise the client may be subtly broken!

  • onDatenBereitAnfrage: a function with the signature async (service, datenBereitAnfrage) => {}
  • onClientStatusAnfrage: a function with the signature async (service, clientStatusAnfrage) => {}
  • onStatusAntwort: a function with the signature async (service, statusAntwort) => {}
  • onSubscribed: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}, bestaetigung, subStats) => {}
  • onSubscriptionExpired: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}, subStats) => {}
  • onSubscriptionCanceled: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}, reason, subStats) => {}
  • onSubscriptionManualFetchStarted: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}) => {}
  • onSubscriptionManualFetchSucceeded: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}, {timePassed}) => {}
  • onSubscriptionManualFetchFailed: a function with the signature async (service, {aboId, aboSubTag, aboSubChildren}) => {}
  • onDatenAbrufenAntwort: a function with the signature async (service, {datensatzAlle, weitereDaten, itLevel, bestaetigung}) => {}
  • onDataFetchStarted: a function with the signature async (service, {datensatzAlle}) => {}
  • onDataFetchSucceeded: a function with the signature async (service, {datensatzAlle}, {nrOfFetches, timePassed}) => {}
  • onDataFetchFailed: a function with the signature async (service, {datensatzAlle}, err, {nrOfFetches, timePassed}) => {}
  • onAusFetchStarted: a function with the signature async ({datensatzAlle}) => {}
  • onAusFetchSucceeded: a function with the signature async ({datensatzAlle}, {nrOfIstFahrts}) => {}
  • onAusFetchFailed: a function with the signature async ({datensatzAlle}, err, {nrOfIstFahrts}) => {}

Related

Contributing

If you have a question or need support using vdv-453-client, please double-check your code and setup first. If you think you have found a bug or want to propose a feature, use the issues page.