Compare commits
10 Commits
chore/patc
...
claude/add
| Author | SHA1 | Date | |
|---|---|---|---|
| 9eeac3cffa | |||
| 6d66e8f25b | |||
| 4764ee6af0 | |||
| 44d2da45d1 | |||
| fe649c81eb | |||
| 946f8dd342 | |||
| 690b2e1559 | |||
| 78f20fa925 | |||
| 90d993736d | |||
| 4d79b842f5 |
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
|||||||
export RATE_LIMIT_WINDOW_MS=${{ secrets.RATE_LIMIT_WINDOW_MS }}
|
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_AFTER_REQUESTS=${{ secrets.RATE_LIMIT_DELAY_AFTER_REQUESTS }}
|
||||||
export RATE_LIMIT_DELAY_MULTIPLIER_MS=${{ secrets.RATE_LIMIT_DELAY_MULTIPLIER_MS }}
|
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 down --remove-orphans
|
||||||
docker compose -f docker-compose.prod.yml up -d --build
|
docker compose -f docker-compose.prod.yml up -d --build
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ services:
|
|||||||
build: .
|
build: .
|
||||||
command: npm run start
|
command: npm run start
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
environment:
|
environment:
|
||||||
@@ -34,18 +36,6 @@ services:
|
|||||||
- ./redis-stack.conf:/redis-stack.conf
|
- ./redis-stack.conf:/redis-stack.conf
|
||||||
command: redis-stack-server /redis-stack.conf
|
command: redis-stack-server /redis-stack.conf
|
||||||
|
|
||||||
caddy:
|
|
||||||
image: caddy:2-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
- "443:443/udp"
|
|
||||||
volumes:
|
|
||||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
||||||
- caddy_data:/data
|
|
||||||
- caddy_config:/config
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
redis_data:
|
||||||
caddy_data:
|
caddy_data:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ scalar DateTime
|
|||||||
type System {
|
type System {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
|
initialRegion: Region
|
||||||
routes: [Route!]
|
routes: [Route!]
|
||||||
route(id: ID): Route
|
route(id: ID): Route
|
||||||
stops: [Stop!]
|
stops: [Stop!]
|
||||||
@@ -16,6 +17,11 @@ type System {
|
|||||||
parkingSystem: ParkingSystem
|
parkingSystem: ParkingSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Region {
|
||||||
|
topLeft: Coordinates!
|
||||||
|
bottomRight: Coordinates!
|
||||||
|
}
|
||||||
|
|
||||||
type ParkingSystem {
|
type ParkingSystem {
|
||||||
systemId: ID!
|
systemId: ID!
|
||||||
parkingStructures: [ParkingStructure!]
|
parkingStructures: [ParkingStructure!]
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { InMemoryExternalSourceETARepository } from "../repositories/shuttle/eta
|
|||||||
import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository";
|
import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository";
|
||||||
import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository";
|
import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository";
|
||||||
import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository";
|
import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository";
|
||||||
|
import { IRegion } from "./SharedEntities";
|
||||||
|
|
||||||
export interface InterchangeSystemBuilderArguments {
|
export interface InterchangeSystemBuilderArguments {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -49,6 +50,12 @@ export interface InterchangeSystemBuilderArguments {
|
|||||||
* at a stop, in latitude/longitude degrees.
|
* at a stop, in latitude/longitude degrees.
|
||||||
*/
|
*/
|
||||||
shuttleStopArrivalDegreeDelta: number;
|
shuttleStopArrivalDegreeDelta: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The initial map region to display when the app first loads
|
||||||
|
* this system. Represents a center coordinate and span.
|
||||||
|
*/
|
||||||
|
initialRegion?: IRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InterchangeSystem {
|
export class InterchangeSystem {
|
||||||
@@ -62,6 +69,7 @@ export class InterchangeSystem {
|
|||||||
public notificationRepository: NotificationRepository,
|
public notificationRepository: NotificationRepository,
|
||||||
public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null,
|
public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null,
|
||||||
public parkingRepository: ParkingGetterSetterRepository | null,
|
public parkingRepository: ParkingGetterSetterRepository | null,
|
||||||
|
public initialRegion: IRegion | null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,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(
|
||||||
@@ -96,6 +104,7 @@ export class InterchangeSystem {
|
|||||||
notificationRepository,
|
notificationRepository,
|
||||||
timedParkingLoader,
|
timedParkingLoader,
|
||||||
parkingRepository,
|
parkingRepository,
|
||||||
|
args.initialRegion ?? null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,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,
|
||||||
@@ -148,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 = {
|
||||||
@@ -205,6 +214,7 @@ export class InterchangeSystem {
|
|||||||
notificationRepository,
|
notificationRepository,
|
||||||
timedParkingLoader,
|
timedParkingLoader,
|
||||||
parkingRepository,
|
parkingRepository,
|
||||||
|
args.initialRegion ?? null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,8 @@ export interface ICoordinates {
|
|||||||
longitude: number;
|
longitude: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRegion {
|
||||||
|
topLeft: ICoordinates;
|
||||||
|
bottomRight: ICoordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [
|
|||||||
name: "Chapman University",
|
name: "Chapman University",
|
||||||
useSelfUpdatingEtas: true,
|
useSelfUpdatingEtas: true,
|
||||||
shuttleStopArrivalDegreeDelta: 0.001,
|
shuttleStopArrivalDegreeDelta: 0.001,
|
||||||
|
initialRegion: {
|
||||||
|
topLeft: { latitude: 33.85733, longitude: -117.89553 },
|
||||||
|
bottomRight: { latitude: 33.73970, longitude: -117.81878 },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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> => ({
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ export const SystemResolvers: Resolvers<ServerContext> = {
|
|||||||
const shuttles = await system.shuttleRepository.getShuttles();
|
const shuttles = await system.shuttleRepository.getShuttles();
|
||||||
return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name));
|
return shuttles.slice().sort((a, b) => a.name.localeCompare(b.name));
|
||||||
},
|
},
|
||||||
|
initialRegion: async (parent, _args, contextValue, _info) => {
|
||||||
|
const system = contextValue.findSystemById(parent.id);
|
||||||
|
if (!system) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return system.initialRegion;
|
||||||
|
},
|
||||||
parkingSystem: async (parent, _args, contextValue, _info) => {
|
parkingSystem: async (parent, _args, contextValue, _info) => {
|
||||||
const system = contextValue.findSystemById(parent.id);
|
const system = contextValue.findSystemById(parent.id);
|
||||||
if (!system) {
|
if (!system) {
|
||||||
|
|||||||
@@ -35,6 +35,37 @@ describe("SystemResolvers", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("initialRegion", () => {
|
||||||
|
const query = `
|
||||||
|
query GetSystemInitialRegion($systemId: ID!) {
|
||||||
|
system(id: $systemId) {
|
||||||
|
initialRegion {
|
||||||
|
topLeft {
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
|
bottomRight {
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
it("returns the initial region for the system", async () => {
|
||||||
|
const response = await getResponseFromQueryNeedingSystemId(query);
|
||||||
|
|
||||||
|
assert(response.body.kind === "single");
|
||||||
|
expect(response.body.singleResult.errors).toBeUndefined();
|
||||||
|
const initialRegion = (response.body.singleResult.data as any).system.initialRegion;
|
||||||
|
expect(initialRegion).toEqual({
|
||||||
|
topLeft: { latitude: 33.85733, longitude: -117.89553 },
|
||||||
|
bottomRight: { latitude: 33.7397, longitude: -117.81878 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("routes", () => {
|
describe("routes", () => {
|
||||||
const query = `
|
const query = `
|
||||||
query GetSystemRoutes($systemId: ID!) {
|
query GetSystemRoutes($systemId: ID!) {
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const systemInfoForTesting: InterchangeSystemBuilderArguments = {
|
|||||||
parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id,
|
parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id,
|
||||||
useSelfUpdatingEtas: false,
|
useSelfUpdatingEtas: false,
|
||||||
shuttleStopArrivalDegreeDelta: 0.001,
|
shuttleStopArrivalDegreeDelta: 0.001,
|
||||||
|
initialRegion: {
|
||||||
|
topLeft: { latitude: 33.85733, longitude: -117.89553 },
|
||||||
|
bottomRight: { latitude: 33.73970, longitude: -117.81878 },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildSystemForTesting() {
|
export function buildSystemForTesting() {
|
||||||
|
|||||||
Reference in New Issue
Block a user