big update
This commit is contained in:
@@ -14,7 +14,7 @@ class ClimateChartWidget implements UIComponent {
|
||||
private readonly skeleton: GridWidget;
|
||||
private chart: Chart | null;
|
||||
private displayMode: DisplayMode = "pastMins";
|
||||
private displayedWin: TimeWindow;
|
||||
private displayedWin: TimeWindow | null;
|
||||
private latestSnapshotInChartTime: number;
|
||||
private readonly canvasElement: HTMLCanvasElement = document.createElement("canvas");
|
||||
|
||||
@@ -23,7 +23,9 @@ class ClimateChartWidget implements UIComponent {
|
||||
...gridProps,
|
||||
body: this.canvasElement,
|
||||
});
|
||||
this.latestSnapshotInChartTime = new Date().getTime() - getAppState().minutesDisplayed * 60000;
|
||||
const now = new Date().getTime();
|
||||
this.latestSnapshotInChartTime = now - getAppState().minutesDisplayed * 60000;
|
||||
this.displayedWin = { start: this.latestSnapshotInChartTime, stop: now };
|
||||
this.setupListeners();
|
||||
this.initChart().catch((e) => {
|
||||
AppStore().setLoading(false);
|
||||
@@ -32,7 +34,7 @@ class ClimateChartWidget implements UIComponent {
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
AppStore().subscribe("displayMode", () => this.update());
|
||||
AppStore().subscribe("displayMode", () => this.updateDisplayMode());
|
||||
AppStore().subscribe("minutesDisplayed", () => this.update());
|
||||
AppStore().subscribe("displayWindow", () => this.update());
|
||||
AppStore().subscribe("snapshots", () => this.update());
|
||||
@@ -44,17 +46,22 @@ class ClimateChartWidget implements UIComponent {
|
||||
await this.update();
|
||||
AppStore().setLoading(false);
|
||||
}
|
||||
|
||||
private async updateDisplayMode() {
|
||||
this.displayMode = getAppState().displayMode;
|
||||
await this.update();
|
||||
}
|
||||
|
||||
private async update() {
|
||||
if (getAppState().displayMode === "window") {
|
||||
await this.updateChartFromTimeWindow(getAppState().displayWindow);
|
||||
this.displayedWin = getAppState().displayWindow;
|
||||
await this.updateChartFromTimeWindow(this.displayedWin);
|
||||
} else if (getAppState().displayMode === "pastMins" && this.displayMode !== "pastMins") {
|
||||
const now = new Date().getTime();
|
||||
const newTimeWindow = { start: now - getAppState().minutesDisplayed * 60000, stop: now };
|
||||
await this.updateChartFromTimeWindow(newTimeWindow);
|
||||
} else {
|
||||
await this.updateChartFromMinuteSpan(getAppState().minutesDisplayed);
|
||||
await this.updateChartFromMinuteSpan();
|
||||
}
|
||||
this.chart.update();
|
||||
}
|
||||
@@ -75,19 +82,22 @@ class ClimateChartWidget implements UIComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async updateChartFromMinuteSpan(mins: number) {
|
||||
private async updateChartFromMinuteSpan() {
|
||||
const now = new Date().getTime();
|
||||
const mins = getAppState().minutesDisplayed;
|
||||
this.removePointsOlderThan(now - mins * 60000);
|
||||
this.appendSnapshots(await AppStore().snapshotsBetween(this.latestSnapshotInChartTime, now));
|
||||
}
|
||||
|
||||
private appendSnapshots(snapshots: Snapshot[]) {
|
||||
for (const snapshot of snapshots.reverse()) {
|
||||
this.humidityPointList().push({x: snapshot.time, y: snapshot.humidity});
|
||||
this.tempPointList().push({x: snapshot.time, y: snapshot.temp});
|
||||
this.co2PointList().push({x: snapshot.time, y: snapshot.co2});
|
||||
if (snapshots.length > 0) {
|
||||
for (const snapshot of snapshots.reverse()) {
|
||||
this.humidityPointList().push({x: snapshot.time, y: snapshot.humidity});
|
||||
this.tempPointList().push({x: snapshot.time, y: snapshot.temp});
|
||||
this.co2PointList().push({x: snapshot.time, y: snapshot.co2});
|
||||
}
|
||||
this.latestSnapshotInChartTime = new Date(snapshots[0].time).getTime();
|
||||
}
|
||||
this.latestSnapshotInChartTime = new Date(snapshots[snapshots.length - 1].time).getTime();
|
||||
}
|
||||
|
||||
private prependSnapshots(snapshots: Snapshot[]) {
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import {AppStore, TimeWindow} from "./StateStore";
|
||||
import Snapshot from "./Snapshot";
|
||||
|
||||
interface SnapshotRecords {
|
||||
snapshots: Snapshot[]
|
||||
}
|
||||
|
||||
class ClimateDataStore {
|
||||
private cachedSpan: TimeWindow;
|
||||
private cache: Snapshot[];
|
||||
|
||||
constructor() {
|
||||
this.cache = [];
|
||||
this.cachedSpan = null;
|
||||
}
|
||||
|
||||
getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
async updateFromWindow(start: number, stop: number) {
|
||||
if (!this.cacheValidForWindow(start, stop)) {
|
||||
await this.fetchMissingSnapshotsBetween(start, stop);
|
||||
}
|
||||
}
|
||||
|
||||
async snapshotsBetween(start: number, stop: number): Promise<Snapshot[]> {
|
||||
if (this.cacheValidForWindow(start, stop)) {
|
||||
return this.cachedBetween(start, stop);
|
||||
}
|
||||
try {
|
||||
await this.fetchMissingSnapshotsBetween(start, stop);
|
||||
return this.cachedBetween(start, stop);
|
||||
} catch (e) {
|
||||
throw new Error(`Server error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
private cacheValidForWindow(start: number, stop: number) {
|
||||
if (this.cachedSpan) {
|
||||
return start >= this.cachedSpan.start && stop <= this.cachedSpan.stop;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMissingSnapshotsBetween(start: number, stop: number) {
|
||||
const dataEndpoint = `${ AppStore().getState().dataEndpointBase }?since=${ new Date(start).toISOString() }`;
|
||||
const payload = await fetch(dataEndpoint);
|
||||
this.cache = ((await payload.json()) as SnapshotRecords).snapshots.reverse();
|
||||
}
|
||||
|
||||
private cachedBetween(start: number, stop: number) {
|
||||
console.log(this.cache.length);
|
||||
const cacheStart = this.findInCacheListRange(start, 0, this.cache.length - 1);
|
||||
const cacheStop = this.findInCacheListRange(stop, 1, this.cache.length);
|
||||
console.log(cacheStart, cacheStop, start, stop, this.cache);
|
||||
return this.cache.slice(cacheStart, cacheStop);
|
||||
}
|
||||
|
||||
private findInCacheListRange(soughtTime: number, listStart: number, listStop: number): number {
|
||||
if (listStop - listStart === 1) {
|
||||
return listStart;
|
||||
} else {
|
||||
const middle = Math.floor((listStop + listStart) / 2);
|
||||
const middleTime = new Date(this.cache[middle].time).getTime();
|
||||
if (middleTime > soughtTime) {
|
||||
return this.findInCacheListRange(soughtTime, listStart, middle);
|
||||
} else if (middleTime < soughtTime) {
|
||||
return this.findInCacheListRange(soughtTime, middle, listStop);
|
||||
} else {
|
||||
return middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ClimateDataStore;
|
||||
75
webapp/src/ListCache.ts
Normal file
75
webapp/src/ListCache.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
type ListCacheLoader<I, D> = (start: I, stop: I) => Promise<D[]>;
|
||||
type ListCacheComparator<I, D> = (data: D, index: I) => -1 | 1 | 0;
|
||||
|
||||
class ListCache<IndexType, DataType> {
|
||||
private cachedSpan: { start: IndexType, stop: IndexType } | null;
|
||||
private cache: DataType[];
|
||||
private loader: ListCacheLoader<IndexType, DataType>;
|
||||
private comparator: ListCacheComparator<IndexType, DataType>;
|
||||
|
||||
constructor(loader: ListCacheLoader<IndexType, DataType>, comparator: ListCacheComparator<IndexType, DataType>) {
|
||||
this.cache = [];
|
||||
this.cachedSpan = null;
|
||||
this.loader = loader;
|
||||
this.comparator = comparator;
|
||||
}
|
||||
|
||||
getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
async snapshotsBetween(start: IndexType, stop: IndexType): Promise<DataType[]> {
|
||||
if (this.cacheValidForWindow(start, stop)) {
|
||||
return this.cachedBetween(start, stop);
|
||||
}
|
||||
try {
|
||||
await this.updateFromWindow(start, stop);
|
||||
return this.cachedBetween(start, stop);
|
||||
} catch (e) {
|
||||
throw new Error(`Server error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateFromWindow(start: IndexType, stop: IndexType) {
|
||||
if (!this.cacheValidForWindow(start, stop)) {
|
||||
await this.fetchMissingElementsBetween(start, stop);
|
||||
}
|
||||
}
|
||||
|
||||
private cacheValidForWindow(start: IndexType, stop: IndexType) {
|
||||
if (this.cachedSpan) {
|
||||
return start >= this.cachedSpan.start && stop <= this.cachedSpan.stop;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchMissingElementsBetween(start: IndexType, stop: IndexType) {
|
||||
this.cache = await this.loader(start, stop);
|
||||
}
|
||||
|
||||
private cachedBetween(start: IndexType, stop: IndexType) {
|
||||
return this.cache.slice(
|
||||
this.findIndexInCache(start),
|
||||
this.findIndexInCache(stop)
|
||||
);
|
||||
}
|
||||
|
||||
private findIndexInCache(soughtVal: IndexType, listStart: number = 0, listStop: number = this.cache.length): number {
|
||||
if (listStop - listStart === 1) {
|
||||
return listStart;
|
||||
} else {
|
||||
const middle = Math.floor((listStop + listStart) / 2);
|
||||
const comparison = this.comparator(this.cache[middle], soughtVal);
|
||||
if (comparison === 1) {
|
||||
return this.findIndexInCache(soughtVal, listStart, middle);
|
||||
} else if (comparison === -1) {
|
||||
return this.findIndexInCache(soughtVal, middle, listStop);
|
||||
} else {
|
||||
return middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListCache;
|
||||
@@ -1,5 +1,5 @@
|
||||
import Snapshot from "./Snapshot";
|
||||
import ClimateDataStore from "./ClimateDataStore";
|
||||
import ListCache from "./ListCache";
|
||||
|
||||
export class AppStateError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -29,10 +29,30 @@ interface AppState {
|
||||
fatalError: Error | null;
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
class AppStateStore {
|
||||
private readonly subscriptions: Record<keyof AppState, Function[]>;
|
||||
private readonly subscriptions: IAppStateSubscriptions;
|
||||
private readonly state: AppState;
|
||||
private readonly climateDataStore: ClimateDataStore = new ClimateDataStore();
|
||||
private readonly climateDataStore: ListCache<number, Snapshot> = new ListCache<number, Snapshot>(
|
||||
async (start, stop) => {
|
||||
const dataEndpoint = `${ this.state.dataEndpointBase }?since=${ new Date(start).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 > index) {
|
||||
return 1;
|
||||
} else if (time < index) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
private initialised: boolean = false;
|
||||
|
||||
constructor(initialState: AppState) {
|
||||
@@ -41,7 +61,7 @@ class AppStateStore {
|
||||
for (const key in this.state) {
|
||||
subscriptions[key] = [];
|
||||
}
|
||||
this.subscriptions = subscriptions as Record<keyof AppState, Function[]>;
|
||||
this.subscriptions = subscriptions as IAppStateSubscriptions;
|
||||
setInterval(() => this.updateClimateData(), this.state.updateIntervalSeconds * 1000);
|
||||
}
|
||||
|
||||
@@ -85,7 +105,7 @@ class AppStateStore {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
subscribe(dataName: keyof AppState, callback: () => any) {
|
||||
subscribe<T extends keyof AppState>(dataName: T, callback: StoreUpdateCallback<AppState[T]>) {
|
||||
this.subscriptions[dataName].push(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"development": false,
|
||||
"development": true,
|
||||
"defaultMinuteSpan": 60,
|
||||
"reloadIntervalSec": 30,
|
||||
"dataEndpoint": "http://tortedda.local/climate/data"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import config from "./config.json";
|
||||
import {initStore} from "./StateStore";
|
||||
import {AppStore, getAppState, initStore} from "./StateStore";
|
||||
import AppUI from "./AppUI";
|
||||
export {config};
|
||||
|
||||
@@ -40,5 +40,7 @@ async function init() {
|
||||
|
||||
document.onreadystatechange = (e) => {
|
||||
init();
|
||||
//@ts-ignore
|
||||
window.store = AppStore();
|
||||
document.onreadystatechange = () => {};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user