Skip to content

public-transport/cached-hafas-client

Repository files navigation

cached-hafas-client

Pass in a HAFAS client, cache data from it.

npm version ISC-licensed support me via GitHub Sponsors chat with me on Twitter

Note: This package is mainly intended to prevent expensive and/or frequent API calls to HAFAS. As a side effect, it may reduce local CPU load & latency, but that depends on the specific use case.

cached-hafas-client's core logic is separated from data storage code; You can pick the store implementation that fits your use case best. Right now the following stores are implemented:

store name built on top of notes
cached-hafas-client/stores/redis.js Redis
cached-hafas-client/stores/sqlite.js SQLite TTL not implemented yet
cached-hafas-client/stores/in-memory.js in-memory (using quick-lru)

Installation

npm install cached-hafas-client

Usage

Let's set up a cached hafas-client instance.

// create HAFAS client
import {createVbbHafas} from 'vbb-hafas'
const hafas = createVbbHafas('my-awesome-program')

// create a store backed by Redis
import Redis from 'ioredis'
import {createRedisStore} from 'cached-hafas-client/stores/redis.js'
const redis = new Redis()
const store = createRedisStore(redis)

// wrap HAFAS client with cache
import {createCachedHafasClient as withCache} from 'cached-hafas-client'
const cachedHafas = withCache(hafas, store)

Because cached-hafas-client caches HAFAS responses by "request signature", it is build on the assumption that, HAFAS works deterministically, aside from the ever-changing transit data underneath. Because there are no guarantees for this, use cached-hafas-client with a grain of salt.

This is why you must send deterministic queries; for example, you must pass opt.duration to departures()/arrivals(), so that cached-hafas-client knows the time frame that the list of results returned by HAFAS is for.

const wollinerStr = '900000007105'
const husemannstr = '900000110511'
const when = new Date(Date.now() + 60 * 60 * 1000)

// will fetch fresh data from HAFAS
await cachedHafas.departures(wollinerStr, {duration: 10, when})

// within the time frame of the departures() call above,
// so it will use the cached data
await cachedHafas.departures(wollinerStr, {
	duration: 3, when: new Date(+when + 3 * 60 * 1000)
})

Note: cached-hafas-client is only compatible with hafas-client@5.

with a custom cache TTL

By default, cached-hafas-client uses TTLs that try to strike a balance between up-to-date-ness and a cache hit ratio: The caching duration depends on how far in the future you query for.

You can pass custom cache TTLs per hafas-client method, either as static values or as a function returning the cache TTL based on the arguments.

const SECOND = 1000
const MINUTE = 60 * SECOND

const cachePeriods = {
	// cache all cachedHafas.stop(…) calls for 10m
	stop: 10 * MINUTE,
	// cache cachedHafas.trip(tripId, opt) based on sqrt(opt.when - now)
	trip: (_, opt = {}) => {
		const diffSecs = (new Date(opt.when) - Date.now()) / SECOND
		if (Number.isNaN(diffSecs)) return 10 * SECOND // fallback
		return Math.round(Math.pow(diffSecs, 1/2) * SECOND)
	},
}
const cachedHafas = withCache(hafas, store, {cachePeriods})

Counting cache hits & misses

cachedHafas.on('hit', (hafasClientMethod, ...args) => {
	console.info('cache hit!', hafasClientMethod, ...args)
})
cachedHafas.on('miss', (hafasClientMethod, ...args) => {
	console.info('cache miss!', hafasClientMethod, ...args)
})

Bypassing the cache

import {CACHED} from 'cached-hafas-client'

// will always fresh data
await cachedHafas.departures(wollinerStr, {[CACHED]: false})

tracking hits & misses as Prometheus metrics

You can optionally track the number of hits & misses as two Prometheus Counters cached_hafas_client_hits_total & cached_hafas_client_misses_total, respectively:

import {trackCachingMetrics} from 'cached-hafas-client/with-metrics.js'

trackCachingMetrics(cachedHafas) // will keep metrics now

API

createCachedHafas(hafas, storage, opt = {})

hafas must be a hafas-client@6-compatible API client.

opt overrides this default configuration:

{
	cachePeriods: {
		departures: 30_1000, arrivals: 30_1000, // 30s
		journeys: 30_1000, // 30s
		refreshJourney: 60_1000, // 1m
		trip: 30_1000, // 30s
		radar: 10_1000, // 10s
		locations: 3_600_1000, // 1h
		stop: 3_600_1000, // 1h
		nearby: 3_600_1000, // 1h
		reachableFrom: 30_1000,
	},
}

Contributing

If you have a question or need support using cached-hafas-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.