2 Commits

Author SHA1 Message Date
9eeac3cffa Remove the migrator utility
It's easier to migrate with a one-time script than bundle it with the codebase. To modify production, pull down the Docker volume there and run the modifications locally before pushing it back up.
2026-03-27 18:34:40 -07:00
6d66e8f25b Add system ID prefix to all Redis keys to prevent cross-system ID clashes
When multiple university systems share the same Redis instance, entity IDs
(shuttles, stops, routes, etc.) could collide. This namespaces all Redis
keys with the system ID (e.g., `1:shuttle:stop:123` instead of
`shuttle:stop:123`).

- Add systemId field and prefixKey() helper to BaseRedisRepository
- Update all Redis repository subclasses to use prefixed keys
- Wire system ID from InterchangeSystem.build() into Redis repositories
- Add migration utility (migrateRedisKeysToSystemPrefix) with tests
- Update all test holders to pass a test system ID

https://claude.ai/code/session_012Vfz1NHWJbVtoDEWcE5tq6
2026-03-23 23:17:13 +00:00
18 changed files with 925 additions and 411 deletions

View File

@@ -1,47 +0,0 @@
name: "Deploy to Coolify"
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
with:
submodules: recursive
- name: Login to Gitea Container Registry
uses: docker/login-action@v4
with:
registry: gitea.bchen.dev
username: ${{ github.actor }}
password: ${{ secrets.PAT }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: gitea.bchen.dev/${{ github.actor }}/project-inter-server:latest
- name: Set up Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
use-cache: false
tailscaled-args: --tun=userspace-networking --socks5-server=localhost:1055 --outbound-http-proxy-listen=localhost:1055
- name: Deploy to Coolify
run: |
curl --request POST '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}'

60
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: "Deploy to birb co."
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v3
# See documentation: https://github.com/tailscale/github-action?tab=readme-ov-file
- name: Connect to Tailscale
uses: tailscale/github-action@v4
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: tag:ci
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan ${{ vars.HOST }} >> ~/.ssh/known_hosts
- name: Remove directory from server
run: |
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
rm -rf ~/${{ vars.DIRECTORY_NAME }}
EOF
# Avoid needing to set up SSH access to GitHub for this user
- name: Transfer repository files to server
run: |
scp -i ~/.ssh/id_ed25519 -r ./* ${{ vars.USERNAME }}@${{ vars.HOST }}:~/${{ vars.DIRECTORY_NAME }}
- name: Deploy on server with Docker
run: |
ssh -i ~/.ssh/id_ed25519 ${{ vars.USERNAME }}@${{ vars.HOST }} << 'EOF'
cd ~/${{ vars.DIRECTORY_NAME }}
export APNS_IS_PRODUCTION=${{ secrets.APNS_IS_PRODUCTION }}
export APNS_BUNDLE_ID=${{ secrets.APNS_BUNDLE_ID }}
export APNS_TEAM_ID=${{ secrets.APNS_TEAM_ID }}
export APNS_KEY_ID=${{ secrets.APNS_KEY_ID }}
export APNS_PRIVATE_KEY=${{ secrets.APNS_PRIVATE_KEY }}
export PARKING_LOGGING_INTERVAL_MS=${{ secrets.PARKING_LOGGING_INTERVAL_MS }}
export RATE_LIMITS_DISABLED=${{ secrets.RATE_LIMITS_DISABLED }}
export RATE_LIMIT_WINDOW_MS=${{ secrets.RATE_LIMIT_WINDOW_MS }}
export RATE_LIMIT_DELAY_AFTER_REQUESTS=${{ secrets.RATE_LIMIT_DELAY_AFTER_REQUESTS }}
export RATE_LIMIT_DELAY_MULTIPLIER_MS=${{ secrets.RATE_LIMIT_DELAY_MULTIPLIER_MS }}
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker compose -f docker-compose.prod.yml down --remove-orphans
docker compose -f docker-compose.prod.yml up -d --build
EOF

View File

@@ -1,7 +1,4 @@
FROM node:22-alpine FROM node:22-alpine
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY . . COPY . .
RUN npm run build
EXPOSE 4000 EXPOSE 4000
CMD ["npm", "run", "start"]

1115
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json", "build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json",
"build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev", "build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev",
"start:dev": "npm run build:dev && node ./dist/index.js", "start:dev": "npm run build:dev && node ./dist/index.js",
"start": "node ./dist/index.js", "start": "npm run build && node ./dist/index.js",
"generate": "graphql-codegen --config codegen.ts", "generate": "graphql-codegen --config codegen.ts",
"test": "npm run build:dev && jest --runInBand" "test": "npm run build:dev && jest --runInBand"
}, },
@@ -26,12 +26,12 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/server": "^5.0.0", "@apollo/server": "^4.11.2",
"@as-integrations/express5": "^1.1.2", "@as-integrations/express5": "^1.1.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.0.1", "express-rate-limit": "^8.0.1",
"express-slow-down": "^3.0.0", "express-slow-down": "^3.0.0",
"graphql": "^16.11.0", "graphql": "^16.10.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"redis": "^4.7.0" "redis": "^4.7.0"
}, },

View File

@@ -91,7 +91,7 @@ export class InterchangeSystem {
); );
notificationScheduler.startListeningForUpdates(); notificationScheduler.startListeningForUpdates();
let { parkingRepository, timedParkingLoader } = await InterchangeSystem.buildRedisParkingLoaderAndRepository(args.parkingSystemId); let { parkingRepository, timedParkingLoader } = await InterchangeSystem.buildRedisParkingLoaderAndRepository(args.parkingSystemId, args.id);
timedParkingLoader?.start(); timedParkingLoader?.start();
return new InterchangeSystem( return new InterchangeSystem(
@@ -145,7 +145,7 @@ export class InterchangeSystem {
shuttleRepository: ShuttleGetterRepository, shuttleRepository: ShuttleGetterRepository,
args: InterchangeSystemBuilderArguments args: InterchangeSystemBuilderArguments
) { ) {
const notificationRepository = new RedisNotificationRepository(); const notificationRepository = new RedisNotificationRepository(undefined, args.id);
await notificationRepository.connect(); await notificationRepository.connect();
const notificationScheduler = new ETANotificationScheduler( const notificationScheduler = new ETANotificationScheduler(
etaRepository, etaRepository,
@@ -157,12 +157,12 @@ export class InterchangeSystem {
return { notificationScheduler, notificationRepository }; return { notificationScheduler, notificationRepository };
} }
private static async buildRedisParkingLoaderAndRepository(id?: string) { private static async buildRedisParkingLoaderAndRepository(id?: string, systemId: string = '') {
if (id === undefined) { if (id === undefined) {
return { parkingRepository: null, timedParkingLoader: null }; return { parkingRepository: null, timedParkingLoader: null };
} }
let parkingRepository: RedisParkingRepository | null = new RedisParkingRepository(); let parkingRepository: RedisParkingRepository | null = new RedisParkingRepository(undefined, systemId);
await parkingRepository.connect(); await parkingRepository.connect();
const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = { const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = {

View File

@@ -4,17 +4,23 @@ import createRedisClientForRepository from '../helpers/createRedisClientForRepos
export abstract class BaseRedisRepository extends EventEmitter { export abstract class BaseRedisRepository extends EventEmitter {
protected redisClient; protected redisClient;
protected readonly systemId: string;
constructor( constructor(
redisClient: RedisClientType = createRedisClientForRepository(), redisClient: RedisClientType = createRedisClientForRepository(),
systemId: string = '',
) { ) {
super(); super();
this.redisClient = redisClient; this.redisClient = redisClient;
this.systemId = systemId;
this.redisClient.on('error', (err) => { this.redisClient.on('error', (err) => {
console.error(err.stack); console.error(err.stack);
}); });
} }
protected prefixKey = (key: string): string =>
this.systemId ? `${this.systemId}:${key}` : key;
get isReady() { get isReady() {
return this.redisClient.isReady; return this.redisClient.isReady;
} }

View File

@@ -1,3 +1,4 @@
import { RedisClientType } from 'redis';
import { TupleKey } from '../../types/TupleKey'; import { TupleKey } from '../../types/TupleKey';
import { import {
Listener, Listener,
@@ -7,14 +8,22 @@ import {
ScheduledNotification ScheduledNotification
} from "./NotificationRepository"; } from "./NotificationRepository";
import { BaseRedisRepository } from "../BaseRedisRepository"; import { BaseRedisRepository } from "../BaseRedisRepository";
import createRedisClientForRepository from '../../helpers/createRedisClientForRepository';
export class RedisNotificationRepository extends BaseRedisRepository implements NotificationRepository { export class RedisNotificationRepository extends BaseRedisRepository implements NotificationRepository {
private notificationListeners: Listener[] = []; private notificationListeners: Listener[] = [];
private readonly NOTIFICATION_KEY_PREFIX = 'notification:'; private readonly NOTIFICATION_KEY_PREFIX = 'notification:';
constructor(
redisClient: RedisClientType = createRedisClientForRepository(),
systemId: string = '',
) {
super(redisClient, systemId);
}
private getNotificationKey = (shuttleId: string, stopId: string): string => { private getNotificationKey = (shuttleId: string, stopId: string): string => {
const tuple = new TupleKey(shuttleId, stopId); const tuple = new TupleKey(shuttleId, stopId);
return `${this.NOTIFICATION_KEY_PREFIX}${tuple.toString()}`; return this.prefixKey(`${this.NOTIFICATION_KEY_PREFIX}${tuple.toString()}`);
}; };
public addOrUpdateNotification = async (notification: ScheduledNotification): Promise<void> => { public addOrUpdateNotification = async (notification: ScheduledNotification): Promise<void> => {

View File

@@ -28,7 +28,7 @@ class RedisNotificationRepositoryHolder implements RepositoryHolder {
url: process.env.REDIS_URL, url: process.env.REDIS_URL,
}); });
await this.redisClient.connect(); await this.redisClient.connect();
this.repo = new RedisNotificationRepository(this.redisClient); this.repo = new RedisNotificationRepository(this.redisClient, 'test-system');
return this.repo; return this.repo;
} }
teardown = async () => { teardown = async () => {

View File

@@ -3,6 +3,8 @@ import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageFilterArguments } from "./ParkingGetterRepository"; import { HistoricalParkingAverageQueryResult, HistoricalParkingAverageFilterArguments } from "./ParkingGetterRepository";
import { BaseRedisRepository } from "../BaseRedisRepository"; import { BaseRedisRepository } from "../BaseRedisRepository";
import { PARKING_LOGGING_INTERVAL_MS } from "../../environment"; import { PARKING_LOGGING_INTERVAL_MS } from "../../environment";
import { RedisClientType } from "redis";
import createRedisClientForRepository from "../../helpers/createRedisClientForRepository";
export type ParkingStructureID = string; export type ParkingStructureID = string;
@@ -10,6 +12,13 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
private dataLastAdded: Map<ParkingStructureID, Date> = new Map(); private dataLastAdded: Map<ParkingStructureID, Date> = new Map();
private loggingIntervalMs = PARKING_LOGGING_INTERVAL_MS; private loggingIntervalMs = PARKING_LOGGING_INTERVAL_MS;
constructor(
redisClient: RedisClientType = createRedisClientForRepository(),
systemId: string = '',
) {
super(redisClient, systemId);
}
addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise<void> => { addOrUpdateParkingStructure = async (structure: IParkingStructure): Promise<void> => {
const keys = this.createRedisKeys(structure.id); const keys = this.createRedisKeys(structure.id);
await this.redisClient.hSet(keys.structure, this.createRedisHashFromStructure(structure)); await this.redisClient.hSet(keys.structure, this.createRedisHashFromStructure(structure));
@@ -28,8 +37,8 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
}; };
clearParkingStructureData = async (): Promise<void> => { clearParkingStructureData = async (): Promise<void> => {
const structureKeys = await this.redisClient.keys('parking:structure:*'); const structureKeys = await this.redisClient.keys(this.prefixKey('parking:structure:*'));
const timeSeriesKeys = await this.redisClient.keys('parking:timeseries:*'); const timeSeriesKeys = await this.redisClient.keys(this.prefixKey('parking:timeseries:*'));
const allKeys = [...structureKeys, ...timeSeriesKeys]; const allKeys = [...structureKeys, ...timeSeriesKeys];
if (allKeys.length > 0) { if (allKeys.length > 0) {
@@ -51,7 +60,7 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
}; };
getParkingStructures = async (): Promise<IParkingStructure[]> => { getParkingStructures = async (): Promise<IParkingStructure[]> => {
const keys = await this.redisClient.keys('parking:structure:*'); const keys = await this.redisClient.keys(this.prefixKey('parking:structure:*'));
const structures: IParkingStructure[] = []; const structures: IParkingStructure[] = [];
for (const key of keys) { for (const key of keys) {
@@ -80,8 +89,8 @@ export class RedisParkingRepository extends BaseRedisRepository implements Parki
}; };
private createRedisKeys = (structureId: string) => ({ private createRedisKeys = (structureId: string) => ({
structure: `parking:structure:${structureId}`, structure: this.prefixKey(`parking:structure:${structureId}`),
timeSeries: `parking:timeseries:${structureId}` timeSeries: this.prefixKey(`parking:timeseries:${structureId}`),
}); });
private createRedisHashFromStructure = (structure: IParkingStructure): Record<string, string> => ({ private createRedisHashFromStructure = (structure: IParkingStructure): Record<string, string> => ({

View File

@@ -25,7 +25,7 @@ class RedisParkingRepositoryHolder implements RepositoryHolder<ParkingGetterSett
url: process.env.REDIS_URL, url: process.env.REDIS_URL,
}); });
await this.redisClient.connect(); await this.redisClient.connect();
this.repo = new RedisParkingRepository(this.redisClient); this.repo = new RedisParkingRepository(this.redisClient, 'test-system');
return this.repo; return this.repo;
}; };
teardown = async () => { teardown = async () => {

View File

@@ -17,8 +17,9 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt
constructor( constructor(
redisClient: RedisClientType = createRedisClientForRepository(), redisClient: RedisClientType = createRedisClientForRepository(),
readonly shuttleStopArrivalDegreeDelta: number = 0.001, readonly shuttleStopArrivalDegreeDelta: number = 0.001,
systemId: string = '',
) { ) {
super(redisClient); super(redisClient, systemId);
} }
get isReady() { get isReady() {
@@ -83,24 +84,24 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt
} }
// Key prefixes for individual entity keys // Key prefixes for individual entity keys
private readonly stopKeyPrefix = 'shuttle:stop:'; private get stopKeyPrefix() { return this.prefixKey('shuttle:stop:'); }
private readonly routeKeyPrefix = 'shuttle:route:'; private get routeKeyPrefix() { return this.prefixKey('shuttle:route:'); }
private readonly shuttleKeyPrefix = 'shuttle:shuttle:'; private get shuttleKeyPrefix() { return this.prefixKey('shuttle:shuttle:'); }
private readonly orderedStopKeyPrefix = 'shuttle:orderedstop:'; private get orderedStopKeyPrefix() { return this.prefixKey('shuttle:orderedstop:'); }
private readonly lastStopKeyPrefix = 'shuttle:laststop:'; private get lastStopKeyPrefix() { return this.prefixKey('shuttle:laststop:'); }
private readonly historicalEtaKeyPrefix = 'shuttle:eta:historical:'; private get historicalEtaKeyPrefix() { return this.prefixKey('shuttle:eta:historical:'); }
// Key patterns for bulk operations (e.g., getting all keys, clearing data) // Key patterns for bulk operations (e.g., getting all keys, clearing data)
private readonly stopKeyPattern = 'shuttle:stop:*'; private get stopKeyPattern() { return this.prefixKey('shuttle:stop:*'); }
private readonly routeKeyPattern = 'shuttle:route:*'; private get routeKeyPattern() { return this.prefixKey('shuttle:route:*'); }
private readonly shuttleKeyPattern = 'shuttle:shuttle:*'; private get shuttleKeyPattern() { return this.prefixKey('shuttle:shuttle:*'); }
private readonly orderedStopKeyPattern = 'shuttle:orderedstop:*'; private get orderedStopKeyPattern() { return this.prefixKey('shuttle:orderedstop:*'); }
private readonly lastStopKeyPattern = 'shuttle:laststop:*'; private get lastStopKeyPattern() { return this.prefixKey('shuttle:laststop:*'); }
/** /**
* Represents a set storing the shuttles that are currently at a stop. * Represents a set storing the shuttles that are currently at a stop.
*/ */
private readonly shuttleIsAtStopKey = 'shuttle:atstop'; private get shuttleIsAtStopKey() { return this.prefixKey('shuttle:atstop'); }
// Helper methods for Redis key generation // Helper methods for Redis key generation
private readonly createStopKey = (stopId: string) => `${this.stopKeyPrefix}${stopId}`; private readonly createStopKey = (stopId: string) => `${this.stopKeyPrefix}${stopId}`;

View File

@@ -31,7 +31,7 @@ class RedisShuttleRepositoryHolder implements RepositoryHolder<ShuttleGetterSett
url: process.env.REDIS_URL, url: process.env.REDIS_URL,
}); });
await this.redisClient.connect(); await this.redisClient.connect();
this.repo = new RedisShuttleRepository(this.redisClient); this.repo = new RedisShuttleRepository(this.redisClient, 0.001, 'test-system');
return this.repo; return this.repo;
}; };
teardown = async () => { teardown = async () => {

View File

@@ -3,11 +3,11 @@ import { BaseRedisRepository } from "../../BaseRedisRepository";
import { ETAGetterRepository, ETARepositoryEvent, ETARepositoryEventListener, ETARepositoryEventName } from "./ETAGetterRepository"; import { ETAGetterRepository, ETARepositoryEvent, ETARepositoryEventListener, ETARepositoryEventName } from "./ETAGetterRepository";
export abstract class BaseRedisETARepository extends BaseRedisRepository implements ETAGetterRepository { export abstract class BaseRedisETARepository extends BaseRedisRepository implements ETAGetterRepository {
private static readonly ETA_KEY_PREFIX = 'shuttle:eta:'; private get etaKeyPrefix() { return this.prefixKey('shuttle:eta:'); }
// Helper methods // Helper methods
protected createEtaKey = (shuttleId: string, stopId: string) => protected createEtaKey = (shuttleId: string, stopId: string) =>
`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:${stopId}`; `${this.etaKeyPrefix}${shuttleId}:${stopId}`;
createRedisHashFromEta = (eta: IEta): Record<string, string> => ({ createRedisHashFromEta = (eta: IEta): Record<string, string> => ({
secondsRemaining: eta.secondsRemaining.toString(), secondsRemaining: eta.secondsRemaining.toString(),
@@ -27,7 +27,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
// Getter implementations // Getter implementations
async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> { async getEtasForShuttleId(shuttleId: string): Promise<IEta[]> {
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}${shuttleId}:*`); const keys = await this.redisClient.keys(`${this.etaKeyPrefix}${shuttleId}:*`);
const etas: IEta[] = []; const etas: IEta[] = [];
for (const key of keys) { for (const key of keys) {
@@ -41,7 +41,7 @@ export abstract class BaseRedisETARepository extends BaseRedisRepository impleme
} }
async getEtasForStopId(stopId: string): Promise<IEta[]> { async getEtasForStopId(stopId: string): Promise<IEta[]> {
const keys = await this.redisClient.keys(`${BaseRedisETARepository.ETA_KEY_PREFIX}*`); const keys = await this.redisClient.keys(`${this.etaKeyPrefix}*`);
const etas: IEta[] = []; const etas: IEta[] = [];
for (const key of keys) { for (const key of keys) {

View File

@@ -2,8 +2,17 @@ import { IEta } from "../../../entities/ShuttleRepositoryEntities";
import { BaseRedisETARepository } from "./BaseRedisETARepository"; import { BaseRedisETARepository } from "./BaseRedisETARepository";
import { ExternalSourceETARepository } from "./ExternalSourceETARepository"; import { ExternalSourceETARepository } from "./ExternalSourceETARepository";
import { ETARepositoryEvent } from "./ETAGetterRepository"; import { ETARepositoryEvent } from "./ETAGetterRepository";
import { RedisClientType } from "redis";
import createRedisClientForRepository from "../../../helpers/createRedisClientForRepository";
export class RedisExternalSourceETARepository extends BaseRedisETARepository implements ExternalSourceETARepository { export class RedisExternalSourceETARepository extends BaseRedisETARepository implements ExternalSourceETARepository {
constructor(
redisClient: RedisClientType = createRedisClientForRepository(),
systemId: string = '',
) {
super(redisClient, systemId);
}
async addOrUpdateEtaFromExternalSource(eta: IEta): Promise<void> { async addOrUpdateEtaFromExternalSource(eta: IEta): Promise<void> {
await this.addOrUpdateEta(eta); await this.addOrUpdateEta(eta);
} }

View File

@@ -13,8 +13,9 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
readonly shuttleRepository: ShuttleGetterRepository, readonly shuttleRepository: ShuttleGetterRepository,
redisClient: RedisClientType = createRedisClientForRepository(), redisClient: RedisClientType = createRedisClientForRepository(),
private referenceTime: Date | null = null, private referenceTime: Date | null = null,
systemId: string = '',
) { ) {
super(redisClient); super(redisClient, systemId);
this.setReferenceTime = this.setReferenceTime.bind(this); this.setReferenceTime = this.setReferenceTime.bind(this);
this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this); this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this);
@@ -28,7 +29,7 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple
} }
private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => {
return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`; return this.prefixKey(`shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`);
} }
setReferenceTime(referenceTime: Date) { setReferenceTime(referenceTime: Date) {

View File

@@ -16,7 +16,7 @@ class RedisExternalSourceETARepositoryHolder implements RepositoryHolder<Externa
url: process.env.REDIS_URL, url: process.env.REDIS_URL,
}); });
await this.redisClient.connect(); await this.redisClient.connect();
this.repo = new RedisExternalSourceETARepository(this.redisClient); this.repo = new RedisExternalSourceETARepository(this.redisClient, 'test-system');
return this.repo; return this.repo;
} }
teardown = async () => { teardown = async () => {

View File

@@ -22,10 +22,12 @@ class RedisSelfUpdatingETARepositoryHolder implements RepositoryHolder<SelfUpdat
}); });
await this.redisClient.connect(); await this.redisClient.connect();
await this.redisClient.flushAll(); await this.redisClient.flushAll();
this.shuttleRepo = new RedisShuttleRepository(this.redisClient); this.shuttleRepo = new RedisShuttleRepository(this.redisClient, 0.001, 'test-system');
this.repo = new RedisSelfUpdatingETARepository( this.repo = new RedisSelfUpdatingETARepository(
this.shuttleRepo, this.shuttleRepo,
this.redisClient, this.redisClient,
null,
'test-system',
); );
return this.repo; return this.repo;
} }