big update - fully functional
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import Snapshot from "./Snapshot";
|
||||
import ListCache from "./ListCache";
|
||||
import Timeseries from "./Timeseries";
|
||||
|
||||
export class AppStateError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -10,6 +9,12 @@ export class AppStateError extends Error {
|
||||
|
||||
export type DisplayMode = "window" | "pastMins";
|
||||
|
||||
export interface EventCallback {
|
||||
newTimeseries: (timeseries: Timeseries) => void;
|
||||
timeseriesUpdated: (timeseries: Timeseries) => void;
|
||||
}
|
||||
type EventCallbackListing<K extends keyof EventCallback> = Record<K, EventCallback[K][]>;
|
||||
|
||||
export interface TimeWindow {
|
||||
start: number;
|
||||
stop: number;
|
||||
@@ -20,7 +25,7 @@ interface AppState {
|
||||
displayWindow: TimeWindow;
|
||||
minutesDisplayed: number;
|
||||
utcOffset: number;
|
||||
snapshots: Snapshot[];
|
||||
timeseries: Timeseries[],
|
||||
overlayText: string;
|
||||
dataEndpointBase: string;
|
||||
updateIntervalSeconds: number;
|
||||
@@ -31,31 +36,15 @@ interface AppState {
|
||||
}
|
||||
|
||||
type StoreUpdateCallback<T> = (newValue?: T, oldValue?: T) => void;
|
||||
type SubscriptionType<T, K extends keyof T> = Record<K, StoreUpdateCallback<T[K]>[]>;
|
||||
type IAppStateSubscriptions = SubscriptionType<AppState, keyof AppState>;
|
||||
type SubscriptionType<K extends keyof AppState> = Record<K, StoreUpdateCallback<AppState[K]>[]>;
|
||||
type IAppStateSubscriptions = SubscriptionType<keyof AppState>;
|
||||
|
||||
|
||||
class AppStateStore {
|
||||
private readonly subscriptions: IAppStateSubscriptions;
|
||||
private readonly eventCallbacks: EventCallbackListing<keyof EventCallback>;
|
||||
private readonly state: AppState;
|
||||
private initialised = false;
|
||||
private loaders = 0;
|
||||
private readonly climateDataStore: ListCache<number, Snapshot> = new ListCache<number, Snapshot>(
|
||||
async (start, stop) => {
|
||||
const dataEndpoint = `${ this.state.dataEndpointBase }/snapshots?from=${ new Date(start).toISOString() }${stop ? `&to=${new Date(stop).toISOString()}` : ""}`;
|
||||
const payload = await fetch(dataEndpoint);
|
||||
return ((await payload.json()) as any).snapshots.reverse();
|
||||
},
|
||||
(data, index) => {
|
||||
const time = new Date(data.time).getTime();
|
||||
if (time + getAppState().updateIntervalSeconds * 1000 > index) {
|
||||
return 1;
|
||||
} else if (time - getAppState().updateIntervalSeconds * 1000< index) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
constructor(initialState: AppState) {
|
||||
this.state = initialState;
|
||||
@@ -63,62 +52,89 @@ class AppStateStore {
|
||||
for (const key in this.state) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
this.eventCallbacks = {newTimeseries: [], timeseriesUpdated: []};
|
||||
this.subscriptions = subscriptions as IAppStateSubscriptions;
|
||||
setInterval(() => this.updateClimateData(), this.state.updateIntervalSeconds * 1000);
|
||||
this.init();
|
||||
setInterval(() => this.getNewTimeseriesData(), this.state.updateIntervalSeconds * 1000);
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.initialised) {
|
||||
await this.updateClimateData();
|
||||
this.initialised = true;
|
||||
}
|
||||
await this.updateTimeseriesFromSettings();
|
||||
await this.getNewTimeseriesData();
|
||||
}
|
||||
|
||||
private notify(subscribedValue: keyof AppState) {
|
||||
addTimeseries(timeseries: Timeseries) {
|
||||
if (this.state.timeseries.indexOf(timeseries) >= 0) {
|
||||
throw new AppStateError("Timeseries has already been added!");
|
||||
}
|
||||
this.state.timeseries.push(timeseries);
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.eventCallbacks["newTimeseries"].forEach(cb => cb(timeseries));
|
||||
this.updateTimeseriesFromSettings();
|
||||
}
|
||||
|
||||
private notifyStoreVal<T extends keyof AppState>(subscribedValue: T, newValue?: AppState[T], oldValue?: AppState[T]) {
|
||||
for (const subscriptionCallback of this.subscriptions[subscribedValue]) {
|
||||
new Promise(() => subscriptionCallback());
|
||||
new Promise(() => subscriptionCallback(newValue, oldValue));
|
||||
}
|
||||
}
|
||||
|
||||
private async updateClimateData() {
|
||||
const now = new Date().getTime();
|
||||
private async updateTimeseriesFromSettings() {
|
||||
let start: number;
|
||||
let stop: number;
|
||||
if (this.state.displayMode === "window") {
|
||||
await this.climateDataStore.updateFromWindow(
|
||||
this.state.displayWindow.start,
|
||||
this.state.displayWindow.stop
|
||||
);
|
||||
start = this.state.displayWindow.start;
|
||||
stop = this.state.displayWindow.stop;
|
||||
} else {
|
||||
await this.climateDataStore.updateFromWindow(
|
||||
now - this.state.minutesDisplayed * 60000,
|
||||
now
|
||||
);
|
||||
start = this.state.lastUpdateTime - this.state.minutesDisplayed * 60;
|
||||
stop = this.state.lastUpdateTime;
|
||||
}
|
||||
this.addLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
await timeseries.updateFromWindow(start, stop);
|
||||
}
|
||||
this.finishLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
|
||||
}
|
||||
this.setLastUpdateTime(now);
|
||||
this.setSnapshots(this.climateDataStore.getCache());
|
||||
}
|
||||
|
||||
async snapshotsBetween(start: number, stop: number) {
|
||||
return this.climateDataStore.snapshotsBetween(start, stop);
|
||||
private async getNewTimeseriesData() {
|
||||
this.addLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
await timeseries.getLatest();
|
||||
}
|
||||
this.finishLoad();
|
||||
for (const timeseries of this.state.timeseries) {
|
||||
this.notifyStoreVal("timeseries");
|
||||
this.eventCallbacks["timeseriesUpdated"].forEach(cb => cb(timeseries));
|
||||
}
|
||||
this.setLastUpdateTime(new Date().getTime() / 1000);
|
||||
}
|
||||
|
||||
getState(): AppState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
subscribe<T extends keyof AppState>(dataName: T, callback: StoreUpdateCallback<AppState[T]>) {
|
||||
subscribeStoreVal<T extends keyof AppState>(dataName: T, callback: StoreUpdateCallback<AppState[T]>) {
|
||||
this.subscriptions[dataName].push(callback);
|
||||
}
|
||||
|
||||
on<T extends keyof EventCallback>(event: T, callback: EventCallback[T]) {
|
||||
this.eventCallbacks[event].push(callback);
|
||||
}
|
||||
|
||||
setDisplayMode(mode: DisplayMode) {
|
||||
this.state.displayMode = mode;
|
||||
this.notify("displayMode");
|
||||
this.notifyStoreVal("displayMode");
|
||||
}
|
||||
|
||||
setDisplayWindow(newWin: TimeWindow) {
|
||||
if (newWin.start < newWin.stop) {
|
||||
this.state.displayWindow = {...newWin};
|
||||
this.notify("displayWindow");
|
||||
this.updateClimateData();
|
||||
this.notifyStoreVal("displayWindow");
|
||||
this.updateTimeseriesFromSettings();
|
||||
} else {
|
||||
throw new AppStateError(`Invalid display window from ${newWin.start} to ${newWin.stop}`);
|
||||
}
|
||||
@@ -127,8 +143,8 @@ class AppStateStore {
|
||||
setMinutesDisplayed(mins: number) {
|
||||
if (mins > 0) {
|
||||
this.state.minutesDisplayed = Math.ceil(mins);
|
||||
this.notify("minutesDisplayed");
|
||||
this.updateClimateData();
|
||||
this.notifyStoreVal("minutesDisplayed");
|
||||
this.updateTimeseriesFromSettings();
|
||||
} else {
|
||||
throw new AppStateError(`Invalid minutes passed: ${mins}`);
|
||||
}
|
||||
@@ -137,16 +153,23 @@ class AppStateStore {
|
||||
setUtcOffset(newOffset: number) {
|
||||
if (Math.floor(newOffset) === newOffset && newOffset <= 14 && newOffset >= -12) {
|
||||
this.state.utcOffset = newOffset;
|
||||
this.notify("snapshots");
|
||||
} else {
|
||||
throw new AppStateError(`Invalid UTC offset: ${newOffset}`);
|
||||
console.warn(`Invalid UTC offset: ${newOffset}`);
|
||||
if (newOffset > 14) {
|
||||
this.state.utcOffset = 14;
|
||||
} else if (newOffset < -12) {
|
||||
this.state.utcOffset = -12;
|
||||
} else {
|
||||
this.state.utcOffset = Math.floor(newOffset);
|
||||
}
|
||||
}
|
||||
this.notifyStoreVal("utcOffset");
|
||||
}
|
||||
|
||||
private setLastUpdateTime(newTime: number) {
|
||||
if (this.state.lastUpdateTime <= newTime) {
|
||||
this.state.lastUpdateTime = newTime;
|
||||
this.notify("lastUpdateTime");
|
||||
this.notifyStoreVal("lastUpdateTime");
|
||||
} else {
|
||||
throw new AppStateError(`Bad new update time was before last update time. Old: ${this.state.lastUpdateTime}, New: ${newTime}`);
|
||||
}
|
||||
@@ -154,36 +177,31 @@ class AppStateStore {
|
||||
|
||||
setOverlayText(text: string) {
|
||||
this.state.overlayText = text;
|
||||
this.notify("overlayText");
|
||||
this.notifyStoreVal("overlayText");
|
||||
}
|
||||
|
||||
addLoad() {
|
||||
this.loaders += 1;
|
||||
this.state.isLoading = this.loaders > 0;
|
||||
this.notify("isLoading");
|
||||
this.notifyStoreVal("isLoading");
|
||||
}
|
||||
|
||||
finishLoad() {
|
||||
this.loaders -= 1;
|
||||
this.state.isLoading = this.loaders > 0;
|
||||
this.notify("isLoading");
|
||||
this.notifyStoreVal("isLoading");
|
||||
}
|
||||
|
||||
fatalError(err: Error) {
|
||||
if (!this.state.fatalError) {
|
||||
this.state.fatalError = err;
|
||||
this.notify("fatalError");
|
||||
this.notifyStoreVal("fatalError");
|
||||
}
|
||||
}
|
||||
|
||||
setDocumentReady(isReady: boolean) {
|
||||
this.state.documentReady = isReady;
|
||||
this.notify("documentReady");
|
||||
}
|
||||
|
||||
private setSnapshots(snapshots: Snapshot[]) {
|
||||
this.state.snapshots = snapshots;
|
||||
this.notify("snapshots");
|
||||
this.notifyStoreVal("documentReady");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +209,6 @@ let store: AppStateStore;
|
||||
|
||||
export async function initStore(initialState: AppState) {
|
||||
store = new AppStateStore(initialState);
|
||||
await store.init();
|
||||
return store;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user