+
diff --git a/typescript/Frontend/src/components/Layout/Search.tsx b/typescript/Frontend/src/components/Layout/Search.tsx
index effd13eed..52bc74d1f 100644
--- a/typescript/Frontend/src/components/Layout/Search.tsx
+++ b/typescript/Frontend/src/components/Layout/Search.tsx
@@ -12,7 +12,7 @@ const SearchSidebar = (props: SearchSidebarProps) => {
const intl = useIntl();
return (
- <>
+
{
onChange={(e) => setSearchQuery(e.target.value)}
/>
- >
+
);
};
diff --git a/typescript/Frontend/src/components/Users/User.tsx b/typescript/Frontend/src/components/Users/User.tsx
index c18572f00..7c6e3ee8d 100644
--- a/typescript/Frontend/src/components/Users/User.tsx
+++ b/typescript/Frontend/src/components/Users/User.tsx
@@ -7,10 +7,10 @@ import { I_User } from "../../util/user.util";
import UserForm from "./UserForm";
import { useIntl } from "react-intl";
-interface I_DetailProps {
+interface I_DetailProps {
hasMoveButton?: boolean;
}
-const Detail = (props: I_DetailProps) => {
+const Detail = (props: I_DetailProps) => {
const { id } = useParams();
const { locale } = useIntl();
const [values, setValues] = useState();
diff --git a/typescript/Frontend/src/components/Users/UserForm.tsx b/typescript/Frontend/src/components/Users/UserForm.tsx
index 8fb9e5f6e..d7ab9f5ec 100644
--- a/typescript/Frontend/src/components/Users/UserForm.tsx
+++ b/typescript/Frontend/src/components/Users/UserForm.tsx
@@ -7,6 +7,7 @@ import { I_User } from "../../util/user.util";
import InnovenergyButton from "../Layout/InnovenergyButton";
import InnovenergyTextfield from "../Layout/InnovenergyTextfield";
import { UserContext } from "../Context/UserContextProvider";
+import { UsersContext } from "../Context/UsersContextProvider";
interface I_UserFormProps {
handleSubmit: (formikValues: Partial) => Promise;
@@ -19,9 +20,11 @@ const UserForm = (props: I_UserFormProps) => {
const [error, setError] = useState();
const intl = useIntl();
- const { currentUser } = useContext(UserContext);
+ const { getCurrentUser } = useContext(UserContext);
+ const { fetchAvailableUsers } = useContext(UsersContext);
- const readOnly = !currentUser?.hasWriteAccess;
+ const currentUser = getCurrentUser();
+ const readOnly = !currentUser.hasWriteAccess;
const formik = useFormik({
initialValues: {
@@ -34,6 +37,7 @@ const UserForm = (props: I_UserFormProps) => {
handleSubmit(formikValues)
.then(() => {
setOpen(true);
+ fetchAvailableUsers();
setLoading(false);
})
.catch((err: AxiosError) => {
@@ -85,7 +89,7 @@ const UserForm = (props: I_UserFormProps) => {
/>
{loading && }
- {currentUser?.hasWriteAccess && (
+ {currentUser.hasWriteAccess && (
diff --git a/typescript/Frontend/src/components/Users/UserTabs.tsx b/typescript/Frontend/src/components/Users/UserTabs.tsx
index c44fac79b..6a3068d02 100644
--- a/typescript/Frontend/src/components/Users/UserTabs.tsx
+++ b/typescript/Frontend/src/components/Users/UserTabs.tsx
@@ -14,26 +14,22 @@ const UserTabs = () => {
if (id) {
return (
-
-
-
-
-
-
-
+
+
+
);
}
return null;
diff --git a/typescript/Frontend/src/components/Users/Users.tsx b/typescript/Frontend/src/components/Users/Users.tsx
index 03cc73db2..accfdbf31 100644
--- a/typescript/Frontend/src/components/Users/Users.tsx
+++ b/typescript/Frontend/src/components/Users/Users.tsx
@@ -11,13 +11,13 @@ import { useContext } from "react";
import { UserContext } from "../Context/UserContextProvider";
const Users = () => {
- const { currentUser } = useContext(UserContext);
+ const { getCurrentUser } = useContext(UserContext);
return (
-
+
- {currentUser?.hasWriteAccess && }
+ {getCurrentUser().hasWriteAccess && }
diff --git a/typescript/Frontend/src/config/axiosConfig.tsx b/typescript/Frontend/src/config/axiosConfig.tsx
index f71357136..38e7e1f81 100644
--- a/typescript/Frontend/src/config/axiosConfig.tsx
+++ b/typescript/Frontend/src/config/axiosConfig.tsx
@@ -10,7 +10,7 @@ const axiosConfig = axios.create({
axiosConfig.defaults.params = {};
axiosConfig.interceptors.request.use(
(config) => {
- const tokenString = sessionStorage.getItem("token");
+ const tokenString = localStorage.getItem("token");
const token = tokenString !== null ? JSON.parse(tokenString) : "";
if (token) {
config.params["authToken"] = token;
diff --git a/typescript/Frontend/src/dataCache/S3/S3Access.ts b/typescript/Frontend/src/dataCache/S3/S3Access.ts
new file mode 100644
index 000000000..6780f0ced
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/S3/S3Access.ts
@@ -0,0 +1,79 @@
+import {sha1Hmac} from "./Sha1";
+import {Utf8} from "./Utf8";
+import {toBase64} from "./UInt8Utils";
+
+export class S3Access
+{
+ constructor
+ (
+ readonly bucket: string,
+ readonly region: string,
+ readonly provider: string,
+ readonly key: string,
+ readonly secret: string,
+ readonly contentType: string
+ )
+ {}
+
+ get host() : string { return `${this.bucket}.${this.region}.${this.provider}` }
+ get url() : string { return `https://${this.host}` }
+
+ public get(s3Path : string): Promise
+ {
+ const method = "GET";
+ const auth = this.createAuthorizationHeader(method, s3Path, "");
+ const url = this.url + "/" + s3Path
+ const headers = {"Host": this.host, "Authorization": auth};
+
+ try
+ {
+ return fetch(url, {method: method, mode: "cors", headers: headers})
+ }
+ catch
+ {
+ return Promise.reject()
+ }
+ }
+
+ private createAuthorizationHeader(method: string,
+ s3Path: string,
+ date: string)
+ {
+ return createAuthorizationHeader
+ (
+ method,
+ this.bucket,
+ s3Path,
+ date,
+ this.key,
+ this.secret,
+ this.contentType
+ );
+ }
+}
+
+function createAuthorizationHeader(method: string,
+ bucket: string,
+ s3Path: string,
+ date: string,
+ s3Key: string,
+ s3Secret: string,
+ contentType: string,
+ md5Hash: string = "")
+{
+ // StringToSign = HTTP-Verb + "\n" +
+ // Content-MD5 + "\n" +
+ // Content-Type + "\n" +
+ // Date + "\n" +
+ // CanonicalizedAmzHeaders +
+ // CanonicalizedResource;
+
+ const payload = Utf8.encode(`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`)
+
+ //console.log(`${method}\n${md5Hash}\n${contentType}\n${date}\n/${bucket}/${s3Path}`)
+
+ const secret = Utf8.encode(s3Secret)
+ const signature = toBase64(sha1Hmac(payload, secret));
+
+ return `AWS ${s3Key}:${signature}`
+}
diff --git a/typescript/Frontend/src/dataCache/S3/Sha1.ts b/typescript/Frontend/src/dataCache/S3/Sha1.ts
new file mode 100644
index 000000000..17dcea21f
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/S3/Sha1.ts
@@ -0,0 +1,125 @@
+import {concat, pad} from "./UInt8Utils";
+
+const BigEndian = false
+
+export function sha1Hmac(msg: Uint8Array, key: Uint8Array): Uint8Array
+{
+ if (key.byteLength > 64)
+ key = sha1(key)
+ if (key.byteLength < 64)
+ key = pad(key, 64)
+
+ const oKey = key.map(b => b ^ 0x5C);
+ const iKey = key.map(b => b ^ 0x36);
+
+ const iData = concat(iKey, msg);
+ const iHash = sha1(iData);
+ const oData = concat(oKey, iHash);
+
+ return sha1(oData);
+}
+
+export function sha1(data: Uint8Array): Uint8Array
+{
+ const paddedData: DataView = initData(data)
+
+ const H = new Uint32Array([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0])
+ const S = new Uint32Array(5) // State
+ const round = new Uint32Array(80);
+
+ function initRound(startOffset: number)
+ {
+ for (let i = 0; i < 16; i++)
+ round[i] = paddedData.getUint32((startOffset + i) * 4, BigEndian);
+
+ for (let i = 16; i < 80; i++)
+ {
+ const int32 = round[i - 3] ^ round[i - 8] ^ round[i - 14] ^ round[i - 16];
+ round[i] = rotate1(int32); // SHA0 has no rotate
+ }
+ }
+
+ const functions =
+ [
+ () => (S[1] & S[2] | ~S[1] & S[3]) + 0x5A827999,
+ () => (S[1] ^ S[2] ^ S[3]) + 0x6ED9EBA1,
+ () => (S[1] & S[2] | S[1] & S[3] | S[2] & S[3]) + 0x8F1BBCDC,
+ () => (S[1] ^ S[2] ^ S[3]) + 0xCA62C1D6
+ ]
+
+ for (let startOffset = 0; startOffset < paddedData.byteLength / 4; startOffset += 16)
+ {
+ initRound(startOffset);
+
+ S.set(H)
+
+ for (let r = 0, i = 0; r < 4; r++)
+ {
+ const f = functions[r]
+ const end = i + 20;
+
+ do
+ {
+ const S0 = rotate5(S[0]) + f() + S[4] + round[i];
+ S[4] = S[3];
+ S[3] = S[2];
+ S[2] = rotate30(S[1]);
+ S[1] = S[0];
+ S[0] = S0;
+ }
+ while (++i < end)
+ }
+
+ for (let i = 0; i < 5; i++)
+ H[i] += S[i]
+ }
+
+ swapEndianness(H);
+
+ return new Uint8Array(H.buffer)
+}
+
+function rotate5(int32: number)
+{
+ return (int32 << 5) | (int32 >>> 27); // >>> for unsigned shift
+}
+
+function rotate30(int32: number)
+{
+ return (int32 << 30) | (int32 >>> 2);
+}
+
+function rotate1(int32: number)
+{
+ return (int32 << 1) | (int32 >>> 31);
+}
+
+function initData(data: Uint8Array): DataView
+{
+ const dataLength = data.length
+ const extendedLength = dataLength + 9; // add 8 bytes for UInt64 length + 1 byte for "stop-bit" (0x80)
+ const paddedLength = Math.ceil(extendedLength / 64) * 64; // pad to 512 bits block
+ const paddedData = new Uint8Array(paddedLength)
+
+ paddedData.set(data)
+ paddedData[dataLength] = 0x80 // append single 1 bit at end of data
+
+ const dataView = new DataView(paddedData.buffer)
+
+ // append UInt64 length
+ dataView.setUint32(paddedData.length - 4, dataLength << 3 , BigEndian) // dataLength in *bits* LO, (<< 3: x8 bits per byte)
+ dataView.setUint32(paddedData.length - 8, dataLength >>> 29, BigEndian) // dataLength in *bits* HI
+
+ return dataView
+}
+
+function swapEndianness(uint32Array: Uint32Array)
+{
+ const dv = new DataView(uint32Array.buffer)
+ for (let i = 0; i < uint32Array.byteLength; i += 4)
+ {
+ const uint32 = dv.getUint32(i, false)
+ dv.setUint32(i, uint32, true)
+ }
+}
+
diff --git a/typescript/Frontend/src/dataCache/S3/UInt8Utils.ts b/typescript/Frontend/src/dataCache/S3/UInt8Utils.ts
new file mode 100644
index 000000000..7e6c5d607
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/S3/UInt8Utils.ts
@@ -0,0 +1,56 @@
+export function pad(data: Uint8Array, length: number): Uint8Array
+{
+ if (length < data.byteLength)
+ throw new RangeError("length")
+
+ const padded = new Uint8Array(length)
+ padded.set(data)
+
+ return padded;
+}
+
+export function concat(left: Uint8Array, right: Uint8Array): Uint8Array
+{
+ const c = new Uint8Array(left.length + right.length);
+ c.set(left);
+ c.set(right, left.length);
+ return c
+}
+
+export function toHexString(data: Uint8Array)
+{
+ return [...data].map(byteToHex).join('');
+}
+
+function byteToHex(b: number)
+{
+ return b.toString(16).padStart(2, "0");
+}
+
+const b64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+export function toBase64(data : Uint8Array) : string
+{
+ const byteLength = data.byteLength
+ const base64LengthPadded = 4 * Math.ceil(byteLength / 3)
+ const base64Length = Math.ceil(byteLength / 3 * 4);
+
+ const base64 = new Array(base64LengthPadded)
+
+ for (let i = 0, o = 0; i < byteLength;)
+ {
+ const x = data[i++]
+ const y = data[i++] ?? 0
+ const z = data[i++] ?? 0
+
+ base64[o++] = b64Chars[x >>> 2]
+ base64[o++] = b64Chars[(x << 4 | y >>> 4) & 63]
+ base64[o++] = b64Chars[(y << 2 | z >>> 6) & 63]
+ base64[o++] = b64Chars[z & 63]
+ }
+
+ for (let i = base64LengthPadded; i > base64Length ;)
+ base64[--i] = "="
+
+ return base64.join('')
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/S3/Utf8.ts b/typescript/Frontend/src/dataCache/S3/Utf8.ts
new file mode 100644
index 000000000..a7ea2951d
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/S3/Utf8.ts
@@ -0,0 +1,10 @@
+export namespace Utf8
+{
+ const encoder = new TextEncoder()
+ const decoder = new TextDecoder()
+
+ export const encode = (text: string): Uint8Array => encoder.encode(text);
+ export const decode = (data: Uint8Array): string => decoder.decode(data);
+}
+
+
diff --git a/typescript/Frontend/src/dataCache/data.ts b/typescript/Frontend/src/dataCache/data.ts
new file mode 100644
index 000000000..8fdb168e1
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/data.ts
@@ -0,0 +1,21 @@
+import { Maybe } from "yup";
+import {Timestamped} from "./types";
+import { isDefined } from "./utils/maybe";
+
+export type DataRecord = Record
+
+export type DataPoint = Timestamped>
+export type RecordSeries = Array
+export type PointSeries = Array>>
+export type DataSeries = Array>
+
+export function getPoints(recordSeries: RecordSeries, series: keyof DataRecord): PointSeries
+{
+ return recordSeries.map(p => ({time: p.time, value: isDefined(p.value) ? p.value[series] : undefined}))
+}
+
+export function getData(recordSeries: RecordSeries, series: keyof DataRecord): DataSeries
+{
+ return recordSeries.map(p => (isDefined(p.value) ? p.value[series] : undefined))
+}
+
diff --git a/typescript/Frontend/src/dataCache/dataCache.ts b/typescript/Frontend/src/dataCache/dataCache.ts
new file mode 100644
index 000000000..71f230a99
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/dataCache.ts
@@ -0,0 +1,169 @@
+/* eslint-disable no-mixed-operators */
+import {TimeSpan, UnixTime} from "./time";
+import {Observable, Subject} from "rxjs";
+import {SkipList} from "./skipList/skipList";
+import {createDispatchQueue} from "./promiseQueue";
+import {SkipListNode} from "./skipList/skipListNode";
+import {RecordSeries} from "./data";
+import { Maybe, isUndefined } from "./utils/maybe";
+
+
+export const FetchResult =
+{
+ notAvailable : "N/A",
+ tryLater : "Try Later"
+} as const
+
+export type FetchResult =
+ | T
+ | typeof FetchResult.notAvailable
+ | typeof FetchResult.tryLater
+
+function reverseBits(x : number): number
+{
+ // https://stackoverflow.com/a/60227327/141397
+
+ x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >> 1;
+ x = (x & 0x33333333) << 2 | (x & 0xCCCCCCCC) >> 2;
+ x = (x & 0x0F0F0F0F) << 4 | (x & 0xF0F0F0F0) >> 4;
+ x = (x & 0x00FF00FF) << 8 | (x & 0xFF00FF00) >> 8;
+ x = (x & 0x0000FFFF) << 16 | (x & 0xFFFF0000) >> 16;
+
+ return x >>> 0;
+}
+
+
+export default class DataCache>
+{
+ private readonly cache: SkipList> = new SkipList>()
+ private readonly resolution: TimeSpan;
+
+ readonly _fetch: (t: UnixTime) => Promise>;
+
+ private readonly fetchQueue = createDispatchQueue(6)
+ private readonly fetching: Set = new Set()
+
+ public readonly gotData: Observable;
+
+ constructor(fetch: (t: UnixTime) => Promise>, resolution: TimeSpan)
+ {
+ this._fetch = fetch;
+ this.resolution = resolution;
+ this.gotData = new Subject()
+ }
+
+ public prefetch(times: Array, clear = true)
+ {
+ if (clear)
+ {
+ this.fetching.clear()
+ this.fetchQueue.clear()
+ }
+
+ const timesWithPriority = times.map((time, index) => ({time, priority: reverseBits(index)}))
+ timesWithPriority.sort((x, y) => x.priority - y.priority)
+
+ for (let i = 0; i < timesWithPriority.length; i++)
+ {
+ const time = timesWithPriority[i].time.round(this.resolution)
+ const t = time.ticks;
+
+ const node = this.cache.find(t);
+ if (node.index !== t)
+ this.fetchData(time);
+ }
+ }
+
+ public get(timeStamp: UnixTime, fetch = true): Maybe
+ {
+ const time = timeStamp.round(this.resolution)
+ const t = time.ticks;
+
+ const node = this.cache.find(t);
+ if (node.index === t)
+ return node.value
+
+ if (fetch)
+ this.fetchData(time);
+
+ return this.interpolate(node, t)
+ }
+
+ public getSeries(sampleTimes: UnixTime[]): RecordSeries
+ {
+ this.prefetch(sampleTimes)
+ return sampleTimes.map(time => ({time, value: this.get(time, false)}))
+ }
+
+ private interpolate(before: SkipListNode>, t: number): Maybe
+ {
+ const dataBefore = before.value
+ const after = before.next[0];
+ const dataAfter = after.value
+
+ if (isUndefined(dataBefore) && isUndefined(dataAfter))
+ return undefined
+
+ if (isUndefined(dataBefore))
+ return dataAfter
+
+ if (isUndefined(dataAfter))
+ return dataBefore
+
+ const p = t - before.index
+ const n = after.index - t
+ const pn = p + n
+
+ let interpolated: Partial> = {}
+
+ //What about string nodes? like Alarms
+ for (const k of Object.keys(dataBefore))
+ {
+ interpolated[k] = (dataBefore[k] * n + dataAfter[k] * p) / pn
+ }
+
+ return interpolated as T
+ }
+
+ private fetchData(time: UnixTime)
+ {
+ const t = time.ticks;
+
+ if (this.fetching.has(t)) // we are already fetching t
+ return
+
+ const fetchTask = () =>
+ {
+ const onSuccess = (data: FetchResult) =>
+ {
+ if (data === FetchResult.tryLater)
+ {
+ console.warn(FetchResult.tryLater)
+ return
+ }
+
+ const value = data === FetchResult.notAvailable ? undefined : data;
+ this.cache.insert(value, t)
+ }
+
+ const onFailure = (_: unknown) =>
+ {
+ console.error(time.ticks + " FAILED!") // should not happen
+ }
+
+ const dispatch = () =>
+ {
+ this.fetching.delete(time.ticks);
+ (this.gotData as Subject).next(time);
+ }
+
+ return this._fetch(time)
+ .then(d => onSuccess(d), f => onFailure(f))
+ .finally(() => dispatch())
+ };
+
+ this.fetching.add(t)
+ this.fetchQueue.dispatch(() => fetchTask());
+ }
+
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/linq.ts b/typescript/Frontend/src/dataCache/linq.ts
new file mode 100644
index 000000000..1d566f445
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/linq.ts
@@ -0,0 +1,20 @@
+// 0. Import Module
+import { initializeLinq, IEnumerable } from "linq-to-typescript"
+// 1. Declare that the JS types implement the IEnumerable interface
+declare global {
+ interface Array extends IEnumerable { }
+ interface Uint8Array extends IEnumerable { }
+ interface Uint8ClampedArray extends IEnumerable { }
+ interface Uint16Array extends IEnumerable { }
+ interface Uint32Array extends IEnumerable { }
+ interface Int8Array extends IEnumerable { }
+ interface Int16Array extends IEnumerable { }
+ interface Int32Array extends IEnumerable { }
+ interface Float32Array extends IEnumerable { }
+ interface Float64Array extends IEnumerable { }
+ interface Map extends IEnumerable<[K, V]> { }
+ interface Set extends IEnumerable { }
+ interface String extends IEnumerable { }
+}
+// 2. Bind Linq Functions to Array, Map, etc
+initializeLinq()
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/observableUtils.ts b/typescript/Frontend/src/dataCache/observableUtils.ts
new file mode 100644
index 000000000..2869e2d60
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/observableUtils.ts
@@ -0,0 +1,35 @@
+import {map, MonoTypeOperatorFunction, Observable, tap} from "rxjs";
+import {fastHash} from "./utils";
+
+type ConcatX = [
+ ...T[0], ...T[1], ...T[2], ...T[3], ...T[4],
+ ...T[5], ...T[6], ...T[7], ...T[8], ...T[9],
+ ...T[10], ...T[11], ...T[12], ...T[13], ...T[14],
+ ...T[15], ...T[16], ...T[17], ...T[18], ...T[19]
+];
+type Flatten =
+ ConcatX<[...{ [K in keyof T]: T[K] extends any[] ? T[K] : [T[K]] }, ...[][]]>
+
+
+export function flatten()
+{
+ return function>(source: Observable)
+ {
+ return source.pipe
+ (
+ map(a => a.flat() as Flatten)
+ )
+ }
+}
+
+type RecursiveObject = T extends object ? T : never;
+
+type Terminals =
+{
+ [Key in keyof TModel]: TModel[Key] extends RecursiveObject
+ ? Terminals
+ : T;
+};
+
+
+
diff --git a/typescript/Frontend/src/dataCache/promiseQueue.ts b/typescript/Frontend/src/dataCache/promiseQueue.ts
new file mode 100644
index 000000000..fd2b6bc0c
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/promiseQueue.ts
@@ -0,0 +1,51 @@
+
+
+export function createDispatchQueue(maxInflight: number, debug = false): { dispatch: (task: () => Promise) => number; clear: () => void }
+{
+ const queue: Array<() => Promise> = []
+
+ let inflight = 0;
+
+ function done()
+ {
+ inflight--
+
+ if (debug && inflight + queue.length === 0)
+ console.log("queue empty")
+
+ if (inflight < maxInflight && queue.length > 0)
+ {
+ const task = queue.pop()!
+ inflight++
+ task().finally(() => done())
+ }
+ }
+
+ function dispatch(task: () => Promise) : number
+ {
+ if (inflight < maxInflight)
+ {
+ inflight++;
+ task().finally(() => done())
+ }
+ else
+ {
+ if (debug && queue.length === 0)
+ console.log("queue in use")
+
+ queue.push(task)
+ }
+
+ return queue.length
+ }
+
+ function clear()
+ {
+ // https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript
+ queue.length = 0
+ if (debug)
+ console.log("queue cleared")
+ }
+
+ return {dispatch, clear}
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/skipList/skipList.ts b/typescript/Frontend/src/dataCache/skipList/skipList.ts
new file mode 100644
index 000000000..24187502d
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/skipList/skipList.ts
@@ -0,0 +1,80 @@
+import {find, findPath, insert, Path, SkipListNode} from "./skipListNode";
+
+export class SkipList
+{
+ public readonly head: SkipListNode;
+ public readonly tail: SkipListNode;
+
+ private readonly nLevels: number;
+
+ private _length = 0
+
+ constructor(nLevels: number = 20)
+ {
+ // TODO: auto-levels
+
+ this.tail =
+ {
+ index: Number.MAX_VALUE,
+ next: [],
+ value: undefined!
+ };
+
+ this.head =
+ {
+ index: Number.MIN_VALUE,
+ next: Array(nLevels).fill(this.tail),
+ value: undefined!
+ };
+
+ this.nLevels = nLevels
+ }
+
+ public find(index: number, startNode = this.head, endNode = this.tail): SkipListNode
+ {
+ return find(index, startNode, endNode)
+ }
+
+ private findPath(index: number, startNode = this.head, endNode = this.tail): Path
+ {
+ return findPath(index, startNode, endNode)
+ }
+
+ public insert(value: T, index: number): SkipListNode
+ {
+ const path = this.findPath(index)
+ const node = path[0];
+
+ if (node.index === index) // overwrite
+ {
+ node.value = value
+ return node
+ }
+
+ const nodeToInsert = {value, index, next: []} as SkipListNode
+
+ const rnd = (Math.random() * (1 << this.nLevels)) << 0;
+
+ for (let level = 0; level < this.nLevels; level++)
+ {
+ insert(nodeToInsert, path[level], level)
+
+ if ((rnd & (1 << level)) === 0)
+ break
+ }
+
+ this._length += 1
+
+ return nodeToInsert;
+ }
+
+ get length(): number
+ {
+ return this._length;
+ }
+
+ // public remove(index: number): void
+ // {
+ // // TODO
+ // }
+}
diff --git a/typescript/Frontend/src/dataCache/skipList/skipListNode.ts b/typescript/Frontend/src/dataCache/skipList/skipListNode.ts
new file mode 100644
index 000000000..d523c6de2
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/skipList/skipListNode.ts
@@ -0,0 +1,52 @@
+import {asMutableArray} from "../types";
+
+export type Next = { readonly next: ReadonlyArray> }
+export type Index = { readonly index: number }
+export type Indexed = Index & { value: T }
+export type SkipListNode = Next & Indexed
+export type Path = SkipListNode[];
+
+export function find(index: number, startNode: SkipListNode, endNode: SkipListNode): SkipListNode
+{
+ let node = startNode
+
+ for (let level = startNode.next.length - 1; level >= 0; level--)
+ node = findOnLevel(index, node, endNode, level)
+
+ return node
+}
+
+export function findOnLevel(index: number, startNode: SkipListNode, endNode: SkipListNode, level: number): SkipListNode
+{
+ let node: SkipListNode = startNode
+
+ while (true)
+ {
+ const next = node.next[level]
+
+ if (index < next.index || endNode.index < next.index)
+ return node
+
+ node = next
+ }
+}
+
+export function findPath(index: number, startNode: SkipListNode, endNode: SkipListNode): Path
+{
+ const path = Array(startNode.next.length - 1)
+ let node = startNode
+
+ for (let level = startNode.next.length - 1; level >= 0; level--)
+ {
+ node = findOnLevel(index, node, endNode, level)
+ path[level] = node
+ }
+
+ return path
+}
+
+export function insert(nodeToInsert: SkipListNode, after: SkipListNode, onLevel: number): void
+{
+ asMutableArray(nodeToInsert.next)[onLevel] = after.next[onLevel]
+ asMutableArray(after.next)[onLevel] = nodeToInsert
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/stringUtils.ts b/typescript/Frontend/src/dataCache/stringUtils.ts
new file mode 100644
index 000000000..e9cbaca4a
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/stringUtils.ts
@@ -0,0 +1,5 @@
+export function trim(str: string, string: string = " "): string
+{
+ const pattern = '^[' + string + ']*(.*?)[' + string + ']*$';
+ return str.replace(new RegExp(pattern), '$1')
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/time.ts b/typescript/Frontend/src/dataCache/time.ts
new file mode 100644
index 000000000..43b8501c5
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/time.ts
@@ -0,0 +1,302 @@
+import {trim} from "./stringUtils";
+
+export class UnixTime
+{
+ private constructor(readonly ticks: number)
+ {
+ }
+
+ public static readonly Epoch = new UnixTime(0)
+
+ public static now(): UnixTime
+ {
+ return UnixTime.fromTicks(Date.now() / 1000)
+ }
+
+ public static fromDate(date: Date): UnixTime
+ {
+ return UnixTime.fromTicks(date.getTime() / 1000)
+ }
+
+ public toDate(): Date
+ {
+ return new Date(this.ticks * 1000)
+ }
+
+ public static fromTicks(ticks: number): UnixTime
+ {
+ return new UnixTime(ticks)
+ }
+
+ public later(timeSpan: TimeSpan): UnixTime
+ {
+ return new UnixTime(this.ticks + timeSpan.ticks)
+ }
+
+ public move(ticks: number): UnixTime
+ {
+ return new UnixTime(this.ticks + ticks)
+ }
+
+ public earlier(timeSpan: TimeSpan): UnixTime
+ {
+ return new UnixTime(this.ticks - timeSpan.ticks)
+ }
+
+ public isEarlierThan(time: UnixTime): boolean
+ {
+ return this.ticks < time.ticks
+ }
+
+ public isEarlierThanOrEqual(time: UnixTime): boolean
+ {
+ return this.ticks <= time.ticks
+ }
+
+ public isLaterThan(time: UnixTime): boolean
+ {
+ return this.ticks > time.ticks
+ }
+
+ public isLaterThanOrEqual(time: UnixTime): boolean
+ {
+ return this.ticks >= time.ticks
+ }
+
+ public isEqual(time: UnixTime): boolean
+ {
+ return this.ticks === time.ticks
+ }
+
+ public isInTheFuture(): boolean
+ {
+ return this.isLaterThan(UnixTime.now())
+ }
+
+ public isInThePast(): boolean
+ {
+ return this.ticks < UnixTime.now().ticks
+ }
+
+ public round(ticks:number) : UnixTime
+ public round(duration: TimeSpan) : UnixTime
+ public round(durationOrTicks: TimeSpan | number) : UnixTime
+ {
+ const ticks = (typeof durationOrTicks === "number") ? durationOrTicks : durationOrTicks.ticks
+
+ return new UnixTime(Math.round(this.ticks / ticks) * ticks)
+ }
+
+ public rangeTo(time: UnixTime): TimeRange
+ {
+ return TimeRange.fromTimes(this, time);
+ }
+
+ public rangeBefore(timeSpan: TimeSpan): TimeRange
+ {
+ return TimeRange.fromTimes(this.earlier(timeSpan), this);
+ }
+
+ public rangeAfter(timeSpan: TimeSpan): TimeRange
+ {
+ return TimeRange.fromTimes(this, this.later(timeSpan));
+ }
+
+ public toString() : string
+ {
+ return this.ticks.toString()
+ }
+}
+
+
+export class TimeSpan
+{
+ private constructor(readonly ticks: number) {}
+
+ get milliSeconds(): number { return this.ticks * 1000 }
+ get seconds() : number { return this.ticks }
+ get minutes() : number { return this.ticks / 60 }
+ get hours() : number { return this.minutes / 60 }
+ get days() : number { return this.hours / 24 }
+ get weeks() : number { return this.days / 7 }
+
+ public static fromTicks (t: number): TimeSpan { return new TimeSpan(t) }
+ public static fromSeconds(t: number): TimeSpan { return TimeSpan.fromTicks(t) }
+ public static fromMinutes(t: number): TimeSpan { return TimeSpan.fromSeconds(t*60) }
+ public static fromHours (t: number): TimeSpan { return TimeSpan.fromMinutes(t*60) }
+ public static fromDays (t: number): TimeSpan { return TimeSpan.fromHours(t*24) }
+ public static fromWeeks (t: number): TimeSpan { return TimeSpan.fromDays(t*7) }
+
+ public static span(from: UnixTime, to: UnixTime) : TimeSpan
+ {
+ return TimeSpan.fromTicks(Math.abs(to.ticks - from.ticks))
+ }
+
+ public add(timeSpan: TimeSpan) : TimeSpan
+ {
+ return TimeSpan.fromTicks(this.ticks + timeSpan.ticks)
+ }
+
+ public subtract(timeSpan: TimeSpan) : TimeSpan
+ {
+ return TimeSpan.fromTicks(this.ticks - timeSpan.ticks)
+ }
+
+ public divide(n: number) : TimeSpan
+ {
+ if (n <= 0)
+ throw 'n must be positive';
+
+ return TimeSpan.fromTicks(this.ticks/n)
+ }
+
+ public multiply(n: number) : TimeSpan
+ {
+ if (n < 0)
+ throw 'n cannot be negative';
+
+ return TimeSpan.fromTicks(this.ticks * n)
+ }
+
+ public round(ticks:number) : TimeSpan
+ public round(duration: TimeSpan) : TimeSpan
+ public round(durationOrTicks: TimeSpan | number) : TimeSpan
+ {
+ const ticks = (typeof durationOrTicks === "number")
+ ? durationOrTicks
+ : durationOrTicks.ticks
+
+ return TimeSpan.fromTicks(Math.round(this.ticks / ticks) * ticks)
+ }
+
+
+ public toString() : string
+ {
+ let dt = 60*60*24*7
+
+ let ticks = this.ticks;
+
+ if (ticks === 0)
+ return "0s"
+
+ ticks = Math.abs(ticks)
+
+ const nWeeks = Math.floor(ticks / dt)
+ ticks -= nWeeks * dt
+
+ dt /= 7
+ const nDays = Math.floor(ticks / dt)
+ ticks -= nDays * dt
+
+ dt /= 24
+ const nHours = Math.floor(ticks / dt)
+ ticks -= nHours * dt
+
+ dt /= 60
+ const nMinutes = Math.floor(ticks / dt)
+ ticks -= nMinutes * dt
+
+ dt /= 60
+ const nSeconds = Math.floor(ticks / dt)
+
+ let s = ""
+
+ if (nWeeks > 0) s += nWeeks .toString() + "w "
+ if (nDays > 0) s += nDays .toString() + "d "
+ if (nHours > 0) s += nHours .toString() + "h "
+ if (nMinutes > 0) s += nMinutes.toString() + "m "
+ if (nSeconds > 0) s += nSeconds.toString() + "s"
+
+ return trim(s);
+ }
+}
+
+export class TimeRange
+{
+ private constructor(private readonly from: number, private readonly to: number)
+ {
+ }
+
+ public get start(): UnixTime
+ {
+ return UnixTime.fromTicks(this.from)
+ }
+
+ public get mid(): UnixTime
+ {
+ return UnixTime.fromTicks((this.from + this.to) / 2)
+ }
+
+
+ public get end(): UnixTime
+ {
+ return UnixTime.fromTicks(this.to)
+ }
+
+ public get duration(): TimeSpan
+ {
+ return TimeSpan.fromTicks(this.to - this.from)
+ }
+
+ public static fromTimes(from: UnixTime, to: UnixTime): TimeRange
+ {
+ return from.isLaterThan(to)
+ ? new TimeRange(to.ticks, from.ticks)
+ : new TimeRange(from.ticks, to.ticks)
+ }
+
+ public isInside(time: number) : boolean;
+ public isInside(time: UnixTime) : boolean;
+ public isInside(time: UnixTime | number)
+ {
+ const t = time instanceof UnixTime ? time.ticks : time
+
+ return t >= this.from && t < this.to
+ }
+
+ public sample(period: TimeSpan): UnixTime[]
+ {
+ const samples = []
+
+ for (let t = this.from; t < this.to; t += period.ticks)
+ samples.push(UnixTime.fromTicks(t));
+
+ return samples
+ }
+
+ public subdivide(n: number) : TimeRange[]
+ {
+ if (n <= 0)
+ throw 'n must be positive';
+
+ const period = TimeSpan.fromTicks(this.duration.ticks / n);
+ if (period === this.duration)
+ return [this];
+
+ const samples = this.sample(period);
+
+ const ranges : TimeRange[] = []
+
+ for (let i = 0; i < samples.length;)
+ ranges.push(TimeRange.fromTimes(samples[i], samples[++i]))
+
+ return ranges
+ }
+
+
+ public earlier(dt: TimeSpan) : TimeRange
+ {
+ return new TimeRange(this.from - dt.ticks, this.to - dt.ticks)
+ }
+
+ public later(dt: TimeSpan) : TimeRange
+ {
+ return new TimeRange(this.from + dt.ticks, this.to + dt.ticks)
+ }
+
+ public move(ticks: number) : TimeRange
+ {
+ return new TimeRange(this.from + ticks, this.to + ticks)
+ }
+}
+
diff --git a/typescript/Frontend/src/dataCache/types.ts b/typescript/Frontend/src/dataCache/types.ts
new file mode 100644
index 000000000..665fd3663
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/types.ts
@@ -0,0 +1,21 @@
+import {UnixTime} from "./time";
+
+export type Timestamped = { time: UnixTime, value: T }
+
+export type Pair = [T1, T2]
+
+export type Position = { readonly x: number, readonly y: number }
+export type Direction = { readonly dx: number, readonly dy: number }
+export type Size = { readonly width: number, readonly height: number }
+export type Rect = Position & Size
+
+export type Mutable = { -readonly [P in keyof T]: T[P] };
+
+export type FieldKey = { [P in keyof T]: T[P] extends (...args: any) => any ? never : P }[keyof T];
+export type AllFields = Pick>;
+export type SomeFields = Partial>
+export const asMutable = (t: T) => (t as Mutable);
+export const asMutableArray = (t: ReadonlyArray) => (t as Array);
+export const cast = (t: unknown) => (t as T);
+
+export type Rename = Pick> & { [P in N]: T[K] }
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/utils.ts b/typescript/Frontend/src/dataCache/utils.ts
new file mode 100644
index 000000000..bbf0350b1
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils.ts
@@ -0,0 +1,119 @@
+import {IEnumerable} from "linq-to-typescript";
+import { isDefined } from "./utils/maybe";
+
+//export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
+
+export type Nothing = Record
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+
+
+
+export function fastHash(str: string): number
+{
+ const signed = str
+ .split('')
+ .reduce((p, c) => ((p << 5) - p) + c.charCodeAt(0) | 0, 0);
+
+ return Math.abs(signed);
+}
+
+
+
+
+
+// export function flattenObject(obj: object) : object
+// {
+// const flattened = {}
+//
+// for (const key of Object.keys(obj))
+// {
+// // @ts-ignore
+// const value = obj[key]
+//
+// if (typeof value === 'object' && value !== null && !Array.isArray(value))
+// {
+// Object.assign(flattened, flattenObject(value))
+// }
+// else
+// {
+// // @ts-ignore
+// flattened[key] = value
+// }
+// }
+//
+// return flattened
+// }
+//return function>(source: Observable)
+
+
+
+export function* pairwise(iterable: Iterable, init?: T): Generator<[T, T]>
+{
+ const it = iterable[Symbol.iterator]()
+
+ let first : T;
+
+ if (isDefined(init))
+ {
+ first = init
+ }
+ else
+ {
+ const f = it.next()
+ if (f.done)
+ return
+
+ first = f.value
+ }
+
+ let second = it.next()
+
+ while(!second.done)
+ {
+ yield [first, second.value]
+ first = second.value
+ second = it.next()
+ }
+}
+
+export function arraysEqual(a: Array, b: Array)
+{
+ if (a === b) return true;
+ if (a.length !== b.length) return false;
+
+ for (let i = 0; i < a.length; ++i)
+ {
+ if (a[i] !== b[i]) return false;
+ }
+
+ return true;
+}
+
+export function mod(a:number, b:number)
+{
+ return ((a % b) + b) % b;
+}
+
+export function clamp(a: number, min: number, max: number)
+{
+ return a > max ? max
+ : a < min ? min
+ : a
+}
+
+
+export function isDST(d : Date)
+{
+ const jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset();
+ const jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset();
+
+ return Math.max(jan, jul) != d.getTimezoneOffset();
+}
+
+export function Transpose(src: IEnumerable>): IEnumerable>
+{
+ return src
+ .selectMany(line => line.select((element, column) => ({element, column})))
+ .groupBy(i => i.column)
+ .select(g => g.select(e => e.element));
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/utils/fileSystem.ts b/typescript/Frontend/src/dataCache/utils/fileSystem.ts
new file mode 100644
index 000000000..f9975e8d8
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/fileSystem.ts
@@ -0,0 +1,46 @@
+import fs from "fs";
+
+export function doesFileExist(path: string): boolean
+{
+ try
+ {
+ fs.accessSync(path, fs.constants.F_OK);
+ return true;
+ }
+ catch (e)
+ {
+ return false;
+ }
+}
+
+export function doesDirExist(path: string): boolean
+{
+ try
+ {
+ fs.readdirSync(path)
+ return true;
+ }
+ catch (e)
+ {
+ return false;
+ }
+}
+
+export function readJsonFile(path: string)
+{
+ const data = fs.readFileSync(path, "utf-8")
+ return JSON.parse(data) as T
+}
+
+export function writeJsonFile(path: string, contents: T)
+{
+ const data = JSON.stringify(contents)
+ return fs.writeFileSync(path, data, "utf-8")
+}
+
+export function writeJsonFilePretty(path: string, contents: T)
+{
+ const data = JSON.stringify(contents,undefined,2)
+ return fs.writeFileSync(path, data, "utf-8")
+}
+
diff --git a/typescript/Frontend/src/dataCache/utils/httpResponse.ts b/typescript/Frontend/src/dataCache/utils/httpResponse.ts
new file mode 100644
index 000000000..c0fa1ee1e
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/httpResponse.ts
@@ -0,0 +1,127 @@
+
+import MimeType from "./mime";
+import fs from "fs";
+import http, {IncomingMessage, ServerResponse} from "http";
+import { Maybe } from "yup";
+import { getLogger } from "./logging";
+import { isDefined, isUndefined } from "./maybe";
+import { Dictionary } from "./utilityTypes";
+import { promisify } from "util";
+import { entries } from "./utils";
+
+
+const log = getLogger("HTTP")
+const readFile = promisify(fs.readFile)
+
+export type HttpResponse = {
+ body: Maybe
+ headers : Dictionary
+ statusCode: number
+}
+
+
+function contentTypeHeader(mimeType: string)
+{
+ return {'Content-type': mimeType};
+}
+
+function forbidden(message = "403 : forbidden", headers: Dictionary = {}): HttpResponse
+{
+ return text(message, 403, headers)
+}
+
+function notFound(message ="404 : not found", headers: Dictionary = {}): HttpResponse
+{
+ return text(message, 404, headers)
+}
+
+function text(text: string, statusCode = 200, headers: Dictionary = {}): HttpResponse
+{
+ return {
+ statusCode: statusCode,
+ headers : {...headers, ...contentTypeHeader('text/plain')},
+ body : text
+ };
+}
+
+function json(json: Dictionary,
+ replacer?: (k: string, v: unknown) => unknown,
+ headers: Dictionary = {}): HttpResponse
+{
+ return {
+ statusCode: 200,
+ headers : {...headers, 'Content-type': MimeType.json},
+ body: JSON.stringify(json, replacer)
+ }
+}
+
+function empty(headers: Dictionary = {}): HttpResponse
+{
+ return {
+ statusCode: 200,
+ headers,
+ body: undefined
+ }
+}
+
+function ok(body: Maybe, headers: Dictionary = {}): HttpResponse
+{
+ return {
+ statusCode: 200,
+ headers,
+ body
+ }
+}
+
+async function file(localRootPath: string, urlPath: string, headers: Dictionary = {}, defaultPath = "/"): Promise
+{
+ if (urlPath.includes('..'))
+ return HTTP.forbidden();
+
+ const localPath = localRootPath + (urlPath === "/" ? defaultPath : urlPath);
+
+ const body = await readFile(localPath).catch(_ => undefined)
+
+ if (isUndefined(body))
+ return HTTP.notFound();
+
+ if (!('Content-type' in headers))
+ {
+ headers = {...headers, ...contentTypeHeader(MimeType.guessFromPath(localPath))}
+ }
+
+ return HTTP.ok(body, headers)
+}
+
+function createServer(serve: (request: IncomingMessage) => Promise)
+{
+ async function wrapServe(request: IncomingMessage, response: ServerResponse): Promise
+ {
+ const r = await serve(request)
+
+ entries(r.headers).forEach(([k, v]) => response.setHeader(k, v as any))
+
+ response.statusCode = r.statusCode
+
+ if (isDefined(r.body))
+ response.end(r.body)
+ else
+ response.end()
+ }
+
+ return http.createServer(wrapServe);
+}
+
+const HTTP =
+{
+ contentTypeHeader,
+ forbidden,
+ notFound,
+ ok,
+ json,
+ empty,
+ file,
+ createServer
+}
+
+export default HTTP;
diff --git a/typescript/Frontend/src/dataCache/utils/linq.ts b/typescript/Frontend/src/dataCache/utils/linq.ts
new file mode 100644
index 000000000..e68de7db4
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/linq.ts
@@ -0,0 +1,21 @@
+// 0. Import Module
+
+import {IEnumerable, initializeLinq} from "linq-to-typescript"
+// 1. Declare that the JS types implement the IEnumerable interface
+declare global {
+ interface Array extends IEnumerable { }
+ interface Uint8Array extends IEnumerable { }
+ interface Uint8ClampedArray extends IEnumerable { }
+ interface Uint16Array extends IEnumerable { }
+ interface Uint32Array extends IEnumerable { }
+ interface Int8Array extends IEnumerable { }
+ interface Int16Array extends IEnumerable { }
+ interface Int32Array extends IEnumerable { }
+ interface Float32Array extends IEnumerable { }
+ interface Float64Array extends IEnumerable { }
+ interface Map extends IEnumerable<[K, V]> { }
+ interface Set extends IEnumerable { }
+ interface String extends IEnumerable { }
+}
+// 2. Bind Linq Functions to Array, Map, etc
+initializeLinq()
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/utils/logging.ts b/typescript/Frontend/src/dataCache/utils/logging.ts
new file mode 100644
index 000000000..ec5bcd1f0
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/logging.ts
@@ -0,0 +1,11 @@
+let subsystemPadding = 0
+
+export function getLogger(subsystem: string): (msg: string) => void
+{
+ subsystemPadding = Math.max(subsystem.length, subsystemPadding)
+
+ // eslint-disable-next-line no-console
+ return (msg: string) => console.log(`${new Date().toLocaleString()} | ${(subsystem.padEnd(subsystemPadding))} | ${msg}`);
+}
+
+
diff --git a/typescript/Frontend/src/dataCache/utils/match.ts b/typescript/Frontend/src/dataCache/utils/match.ts
new file mode 100644
index 000000000..71106e986
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/match.ts
@@ -0,0 +1,176 @@
+import {Dictionary, Func, Normalize1} from "./utilityTypes";
+import {isUndefined} from "./maybe";
+import {UnionToIntersection} from "simplytyped";
+import {current} from "immer";
+import { keys, valueToFunction } from "./utils";
+
+// Type Compatibility
+// https://www.typescriptlang.org/docs/handbook/type-compatibility.html
+
+
+//TODO: review
+export type IsUnionCase =
+ T extends Dictionary
+ ? [UnionToIntersection] extends [keyof T]
+ ? [keyof T] extends [UnionToIntersection]
+ ? true
+ : false
+ : false
+ : false
+
+//TODO: review
+export type IsTaggedUnion = true extends UnionToIntersection> ? Dictionary : never
+
+export type Unwrap> = UnionToIntersection[keyof UnionToIntersection] ;
+
+export function update>(u: U, e: Partial>)
+{
+ const v = u as UnionToIntersection
+ const o = current(v)
+
+ const ks = keys(v)
+
+ if (ks.length != 1)
+ throw new Error("not a valid union case")
+
+ const tag = ks[0]
+
+ const before = v[tag];
+ const before2 = current(before);
+
+
+ const newVar = {...before, ...e};
+ v[tag] = newVar
+}
+
+export function unwrap>(u: U) : Normalize1>
+{
+ const v = u as UnionToIntersection
+
+ const ks = keys(v)
+
+ if (ks.length != 1)
+ throw new Error("not a valid union case")
+
+ const key = ks[0]
+ return v[key] as any;
+}
+
+export function base>(u: U): Normalize1 & Partial>>>
+{
+ return unwrap(u) as Normalize1 & Partial>>>
+}
+
+export function tag>(u: U) : keyof UnionToIntersection
+{
+ const v = u as UnionToIntersection
+
+ const ks = keys(v)
+
+ if (ks.length != 1)
+ throw new Error("not a valid union case")
+
+ return ks[0]
+}
+
+export function tagsEqual>(u: U, v: U) : v is U
+{
+ return tag(u) === tag(v)
+}
+
+
+type MapFuncs = { [k in keyof UnionToIntersection]: Func[k]> }
+type OtherwiseKeys = Exclude, keyof M>;
+
+type OtherwiseArg = {
+ [k in keyof UnionToIntersection]: Record[k]>
+}[OtherwiseKeys]
+
+type OtherwiseFunc>, R> = Func extends never ? unknown : OtherwiseArg, R>;
+
+
+export function match, M extends Partial>, R>(uCase: U, matchFuncs: M, otherwise: OtherwiseFunc | R):{ [k in keyof M]: M[k] extends Func ? O : never }[keyof M] | R
+{
+ const otw = valueToFunction(otherwise)
+
+ const c = uCase as UnionToIntersection
+
+ const ks = keys(c)
+
+ if (ks.length != 1)
+ return otw(c)
+
+ const key = ks[0]
+ const arg = c[key]
+
+ const matchFunc = matchFuncs[key]
+
+ if (isUndefined(matchFunc))
+ return otw(c);
+
+ return matchFunc(arg as any) as any;
+}
+
+
+export function dispatch>()
+{
+ // type Intersection = UnionToIntersection;
+ //
+ // type MapFuncs = { [k in keyof Intersection]: Func }
+ // type OtherwiseKeys = Exclude;
+ //
+ // type OtherwiseArg = {
+ // [k in keyof Intersection]: Record
+ // }[OtherwiseKeys]
+ //
+ // type OtherwiseFunc, R> = Func extends never ? unknown : OtherwiseArg, R>;
+
+ return >, R>(matchFuncs: M, otherwise: OtherwiseFunc | R) =>
+ {
+ const otw = valueToFunction(otherwise)
+
+ return (uCase: U): { [k in keyof M]: M[k] extends Func ? O : never }[keyof M] | R =>
+ {
+ const c = uCase as UnionToIntersection
+
+ const ks = keys(c)
+
+ if (ks.length != 1)
+ return otw(c)
+
+ const key = ks[0]
+ const arg = c[key]
+
+ const matchFunc = matchFuncs[key]
+
+ if (isUndefined(matchFunc))
+ return otw(c);
+
+ return matchFunc(arg as any) as any;
+ }
+ };
+}
+
+
+export function concat, T extends Dictionary>(rec: R, t:T)
+{
+
+ const result = {} as {
+ [k in keyof UnionToIntersection]: Record[k] & T>
+ }[keyof UnionToIntersection]
+
+ for (const k in rec)
+ {
+
+ // @ts-ignore
+ result[k] = { ...rec[k], ...t}
+ }
+
+ return result
+}
+
+
+
+
+
+
diff --git a/typescript/Frontend/src/dataCache/utils/maybe.ts b/typescript/Frontend/src/dataCache/utils/maybe.ts
new file mode 100644
index 000000000..7ab47fdb9
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/maybe.ts
@@ -0,0 +1,16 @@
+export type Maybe = T | undefined | null;
+
+export function isDefined(e: Maybe): e is T
+{
+ return e != undefined // != by design to include null
+}
+
+export function isUndefined(e: Maybe): e is undefined | null
+{
+ return e == undefined // == by design to include null
+}
+
+export function toArray(e: Maybe): T[]
+{
+ return isDefined(e) ? [e] : []
+}
diff --git a/typescript/Frontend/src/dataCache/utils/milliseconds.ts b/typescript/Frontend/src/dataCache/utils/milliseconds.ts
new file mode 100644
index 000000000..7d0daf94a
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/milliseconds.ts
@@ -0,0 +1,18 @@
+
+export type Milliseconds = number
+
+export const Milliseconds =
+{
+ fromSeconds: (count: number): Milliseconds => count * 1000,
+ fromMinutes: (count: number): Milliseconds => count * 1000 * 60,
+ fromHours : (count: number): Milliseconds => count * 1000 * 60 * 60,
+ fromDays : (count: number): Milliseconds => count * 1000 * 60 * 60 * 24,
+ fromWeeks : (count: number): Milliseconds => count * 1000 * 60 * 60 * 24 * 7,
+
+ toSeconds: (count: Milliseconds): number => count / 1000,
+ toMinutes: (count: Milliseconds): number => count / 1000 / 60,
+ toHours : (count: Milliseconds): number => count / 1000 / 60 / 60,
+ toDays : (count: Milliseconds): number => count / 1000 / 60 / 60 / 24,
+ toWeeks : (count: Milliseconds): number => count / 1000 / 60 / 60 / 24 / 7,
+} as const
+
diff --git a/typescript/Frontend/src/dataCache/utils/mime.ts b/typescript/Frontend/src/dataCache/utils/mime.ts
new file mode 100644
index 000000000..87dc60150
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/mime.ts
@@ -0,0 +1,32 @@
+import PlatformPath from "path";
+import { isDefined } from "./maybe";
+
+
+function guessFromPath(path: string) : string
+{
+ const ext = PlatformPath.parse(path).ext?.substring(1) as keyof typeof MimeType;
+
+ const mimeType = MimeType[ext]
+
+ return isDefined(mimeType) && typeof mimeType === "string"
+ ? mimeType
+ : 'application/octet-stream'
+}
+
+const MimeType =
+{
+ ico : 'image/x-icon',
+ html: 'text/html; charset=UTF-8',
+ js : 'text/javascript',
+ json: 'application/json; charset=UTF-8',
+ css : 'text/css; charset=UTF-8',
+ png : 'image/png',
+ jpg : 'image/jpeg',
+ wav : 'audio/wav',
+ mp3 : 'audio/mpeg',
+ svg : 'image/svg+xml; charset=UTF-8',
+ pdf : 'application/pdf',
+ guessFromPath
+};
+
+export default MimeType
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/utils/path.ts b/typescript/Frontend/src/dataCache/utils/path.ts
new file mode 100644
index 000000000..6d82809b2
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/path.ts
@@ -0,0 +1,81 @@
+import {isUndefined} from "./maybe";
+import {from, IEnumerable} from "linq-to-typescript";
+import {isBoolean, isNumber, isPlainObject, isString} from "./runtimeTypeChecking";
+
+function getAt(root: any, path: (keyof any)[])
+{
+ return path.reduce((v, p) => v[p], root)
+}
+
+
+function iterate(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
+{
+ if (isUndefined(root))
+ return []
+
+ return from(iterate(root))
+
+ function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
+ {
+ if (isString(node) || isNumber(node) || isBoolean(node))
+ yield {path, node}
+ else if (isPlainObject(node))
+ for (const key in node)
+ {
+ path.push(key)
+ yield {path, node}
+ yield* iterate(node[key], path)
+ path.pop()
+ }
+ }
+}
+
+function iterateLeafs(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
+{
+ if (isUndefined(root))
+ return []
+
+ return from(iterate(root))
+
+ function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
+ {
+ if (isString(node) || isNumber(node) || isBoolean(node))
+ yield {path, node}
+ else if (isPlainObject(node))
+ for (const key in node)
+ {
+ path.push(key)
+ yield* iterate(node[key], path)
+ path.pop()
+ }
+ }
+}
+
+
+function iterateBranches(root: unknown): IEnumerable<{ path: string[]; node: unknown }>
+{
+ if (isUndefined(root))
+ return []
+
+ return from(iterate(root))
+
+ function* iterate(node: unknown, path: string[] = []): Generator<{ path: string[]; node: unknown }>
+ {
+ if (isPlainObject(node))
+ for (const key in node)
+ {
+ path.push(key)
+ yield {path, node}
+ yield* iterate(node[key], path)
+ path.pop()
+ }
+ }
+}
+
+export const Path =
+{
+ iterate,
+ iterateLeafs,
+ iterateBranches,
+ getAt
+} as const
diff --git a/typescript/Frontend/src/dataCache/utils/requestUtils.ts b/typescript/Frontend/src/dataCache/utils/requestUtils.ts
new file mode 100644
index 000000000..8757cb3cc
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/requestUtils.ts
@@ -0,0 +1,45 @@
+import {IncomingMessage} from "http";
+import {firstValueFrom, map, Observable, startWith, toArray} from "rxjs";
+
+export function observeData(request: IncomingMessage, maxLength: number = Number.POSITIVE_INFINITY): Observable
+{
+ let nBytes = 0;
+
+ return new Observable(subscriber =>
+ {
+ request.on('end', () => subscriber.complete());
+ request.on('data', (data: Uint8Array) =>
+ {
+ nBytes += data.byteLength
+
+ if (nBytes <= maxLength)
+ subscriber.next(data);
+ else
+ {
+ const error = `too much data: expected ${maxLength} bytes or less, got ${nBytes} bytes.`;
+ subscriber.error(error);
+ request.destroy(new Error(error))
+ }
+ });
+ });
+}
+
+export async function getRequestJson(request: IncomingMessage, maxLength = 500000): Promise
+{
+ const data = await getData(request, maxLength)
+ return JSON.parse(data.toString())
+}
+
+const noData = new Uint8Array(0);
+
+export function getData(request: IncomingMessage, maxLength: number = Number.POSITIVE_INFINITY): Promise
+{
+ const data = observeData(request, maxLength).pipe
+ (
+ startWith(noData),
+ toArray(),
+ map(b => Buffer.concat(b)), // cannot inline!
+ )
+
+ return firstValueFrom(data);
+}
\ No newline at end of file
diff --git a/typescript/Frontend/src/dataCache/utils/runtimeTypeChecking.ts b/typescript/Frontend/src/dataCache/utils/runtimeTypeChecking.ts
new file mode 100644
index 000000000..c24de1606
--- /dev/null
+++ b/typescript/Frontend/src/dataCache/utils/runtimeTypeChecking.ts
@@ -0,0 +1,66 @@
+export type TypeCode =
+ | "undefined"
+ | "object"
+ | "boolean"
+ | "number"
+ | "string"
+ | "function"
+ | "symbol"
+ | "bigint";
+
+export type PlainObject