speed up the load of Live View and Battery View in Sodistore Home
This commit is contained in:
parent
2e52b9ee15
commit
401d82ea7a
|
|
@ -5,6 +5,66 @@ import { S3Access } from 'src/dataCache/S3/S3Access';
|
||||||
import { JSONRecordData, parseChunkJson } from '../Log/graph.util';
|
import { JSONRecordData, parseChunkJson } from '../Log/graph.util';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
// Find the latest chunk file in S3 using ListObjects (single HTTP request)
|
||||||
|
// Returns the parsed chunk data or FetchResult.notAvailable
|
||||||
|
export const fetchLatestDataJson = (
|
||||||
|
s3Credentials?: I_S3Credentials,
|
||||||
|
maxAgeSeconds: number = 400
|
||||||
|
): Promise<FetchResult<Record<string, JSONRecordData>>> => {
|
||||||
|
if (!s3Credentials || !s3Credentials.s3Bucket) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use marker to skip files older than maxAgeSeconds
|
||||||
|
const oldestTimestamp = Math.floor(Date.now() / 1000) - maxAgeSeconds;
|
||||||
|
const marker = `${oldestTimestamp}.json`;
|
||||||
|
|
||||||
|
return s3Access
|
||||||
|
.list(marker, 50)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status !== 200) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
const xml = await r.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(xml, 'application/xml');
|
||||||
|
const keys = Array.from(doc.getElementsByTagName('Key'))
|
||||||
|
.map((el) => el.textContent)
|
||||||
|
.filter((key) => key && /^\d+\.json$/.test(key))
|
||||||
|
.sort((a, b) => Number(b.replace('.json', '')) - Number(a.replace('.json', '')));
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the most recent chunk file
|
||||||
|
const latestKey = keys[0];
|
||||||
|
const res = await s3Access.get(latestKey);
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsontext = await res.text();
|
||||||
|
const byteArray = Uint8Array.from(atob(jsontext), (c) =>
|
||||||
|
c.charCodeAt(0)
|
||||||
|
);
|
||||||
|
const zip = await JSZip.loadAsync(byteArray);
|
||||||
|
const jsonContent = await zip.file('data.json').async('text');
|
||||||
|
return parseChunkJson(jsonContent);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchDataJson = (
|
export const fetchDataJson = (
|
||||||
timestamp: UnixTime,
|
timestamp: UnixTime,
|
||||||
s3Credentials?: I_S3Credentials,
|
s3Credentials?: I_S3Credentials,
|
||||||
|
|
|
||||||
|
|
@ -111,20 +111,52 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
return btoa(String.fromCharCode(...combined));
|
return btoa(String.fromCharCode(...combined));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchDataPeriodically = async () => {
|
// Probe multiple timestamps in parallel, return first successful result
|
||||||
var timeperiodToSearch = 350;
|
const probeTimestampBatch = async (
|
||||||
let res;
|
offsets: number[]
|
||||||
let timestampToFetch;
|
): Promise<{ res: any; timestamp: UnixTime } | null> => {
|
||||||
|
const now = UnixTime.now();
|
||||||
|
const promises = offsets.map(async (offset) => {
|
||||||
|
const ts = now.earlier(TimeSpan.fromSeconds(offset));
|
||||||
|
const result = await fetchDataJson(ts, s3Credentials, false);
|
||||||
|
if (result !== FetchResult.notAvailable && result !== FetchResult.tryLater) {
|
||||||
|
return { res: result, timestamp: ts };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
for (var i = 0; i < timeperiodToSearch; i += 30) {
|
const results = await Promise.all(promises);
|
||||||
|
// Return the most recent hit (smallest offset = first in array)
|
||||||
|
return results.find((r) => r !== null) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataPeriodically = async () => {
|
||||||
|
let res;
|
||||||
|
let timestampToFetch: UnixTime;
|
||||||
|
|
||||||
|
// Search backward in parallel batches of 10 timestamps (2s apart)
|
||||||
|
// Each batch covers 20 seconds, so 20 batches cover 400 seconds
|
||||||
|
const batchSize = 10;
|
||||||
|
const step = 2; // 2-second steps to match even-rounding granularity
|
||||||
|
const maxAge = 400;
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (let batchStart = 0; batchStart < maxAge; batchStart += batchSize * step) {
|
||||||
if (!continueFetching.current) {
|
if (!continueFetching.current) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
const offsets = [];
|
||||||
|
for (let j = 0; j < batchSize; j++) {
|
||||||
|
const offset = batchStart + j * step;
|
||||||
|
if (offset < maxAge) offsets.push(offset);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
const hit = await probeTimestampBatch(offsets);
|
||||||
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
if (hit) {
|
||||||
|
res = hit.res;
|
||||||
|
timestampToFetch = hit.timestamp;
|
||||||
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -133,7 +165,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i >= timeperiodToSearch) {
|
if (!found) {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -154,10 +186,12 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
await timeout(2000);
|
await timeout(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60));
|
// Advance by 150s to find the next chunk (15 records × 10s interval)
|
||||||
|
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(150));
|
||||||
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
|
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
|
||||||
|
|
||||||
for (i = 0; i < 30; i++) {
|
let foundNext = false;
|
||||||
|
for (var i = 0; i < 60; i++) {
|
||||||
if (!continueFetching.current) {
|
if (!continueFetching.current) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -169,6 +203,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
res !== FetchResult.notAvailable &&
|
res !== FetchResult.notAvailable &&
|
||||||
res !== FetchResult.tryLater
|
res !== FetchResult.tryLater
|
||||||
) {
|
) {
|
||||||
|
foundNext = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -177,24 +212,30 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
|
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
|
||||||
}
|
}
|
||||||
if (i == 30) {
|
if (!foundNext) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDataForOneTime = async () => {
|
const fetchDataForOneTime = async () => {
|
||||||
var timeperiodToSearch = 300; // 5 minutes to cover ~2 upload cycles (150s each)
|
// Search backward in parallel batches of 10 timestamps (2s apart)
|
||||||
|
const batchSize = 10;
|
||||||
|
const step = 2;
|
||||||
|
const maxAge = 400;
|
||||||
let res;
|
let res;
|
||||||
let timestampToFetch;
|
|
||||||
|
|
||||||
// Search from NOW backward to find the most recent data
|
for (let batchStart = 0; batchStart < maxAge; batchStart += batchSize * step) {
|
||||||
// Step by 50 seconds - data is uploaded every ~150s, so finer steps are wasteful
|
const offsets = [];
|
||||||
for (var i = 0; i < timeperiodToSearch; i += 50) {
|
for (let j = 0; j < batchSize; j++) {
|
||||||
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
const offset = batchStart + j * step;
|
||||||
|
if (offset < maxAge) offsets.push(offset);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res = await fetchDataJson(timestampToFetch, s3Credentials, false);
|
const hit = await probeTimestampBatch(offsets);
|
||||||
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
|
if (hit) {
|
||||||
|
res = hit.res;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -203,11 +244,12 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i >= timeperiodToSearch) {
|
if (!res) {
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
|
|
@ -215,12 +257,6 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
||||||
const timestamps = Object.keys(res).sort((a, b) => Number(b) - Number(a));
|
const timestamps = Object.keys(res).sort((a, b) => Number(b) - Number(a));
|
||||||
const latestTimestamp = timestamps[0];
|
const latestTimestamp = timestamps[0];
|
||||||
setValues(res[latestTimestamp]);
|
setValues(res[latestTimestamp]);
|
||||||
// setValues(
|
|
||||||
// extractValues({
|
|
||||||
// time: UnixTime.fromTicks(parseInt(timestamp, 10)),
|
|
||||||
// value: res[timestamp]
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,22 @@ export class S3Access {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public list(marker?: string, maxKeys: number = 50): Promise<Response> {
|
||||||
|
const method = "GET";
|
||||||
|
const auth = this.createAuthorizationHeader(method, "", "");
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (marker) params.set("marker", marker);
|
||||||
|
params.set("max-keys", maxKeys.toString());
|
||||||
|
const url = this.url + "/" + this.bucket + "/?" + params.toString();
|
||||||
|
const headers = { Host: this.host, Authorization: auth };
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fetch(url, { method: method, mode: "cors", headers: headers });
|
||||||
|
} catch {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private createAuthorizationHeader(
|
private createAuthorizationHeader(
|
||||||
method: string,
|
method: string,
|
||||||
s3Path: string,
|
s3Path: string,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue