implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+60 -60
View File
@@ -3,113 +3,113 @@ const ENCRYPTION_ALG = "AES-GCM";
// minimal
export async function encryptString(
plaintext: string,
passphrase: string
plaintext: string,
passphrase: string,
): Promise<string> {
if (!window.crypto.subtle) return plaintext;
if (!window.crypto.subtle) return plaintext;
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
}
export async function decryptString(
cyphertext: string,
passphrase: string
cyphertext: string,
passphrase: string,
): Promise<string> {
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
}
// process
export function encode(string: string): Uint8Array {
return new TextEncoder().encode(string);
return new TextEncoder().encode(string);
}
export function decode(array: Uint8Array): string {
return new TextDecoder("utf-8").decode(array);
return new TextDecoder("utf-8").decode(array);
}
export async function encrypt(
iv: Uint8Array,
key: CryptoKey,
message: string
iv: Uint8Array,
key: CryptoKey,
message: string,
): Promise<Uint8Array> {
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message)
);
return new Uint8Array(arrayBuffer);
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message),
);
return new Uint8Array(arrayBuffer);
}
export async function decrypt(
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array,
): Promise<string> {
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext
);
return arrayBufferToString(arrayBuffer);
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext,
);
return arrayBufferToString(arrayBuffer);
}
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
return await crypto.subtle.digest("SHA-256", encoded);
return await crypto.subtle.digest("SHA-256", encoded);
}
// iv
export function generateIV(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
}
export function splitMergedString(mergedString: string): [Uint8Array, string] {
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
return [JSON.parse(iv), encryptedString];
return [JSON.parse(iv), encryptedString];
}
// key
export async function importKey(
passphrase: string,
purpose: "encrypt" | "decrypt"
passphrase: string,
purpose: "encrypt" | "decrypt",
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose]
);
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose],
);
}
// conversion
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
}
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
return Array.from(uInt8Array);
return Array.from(uInt8Array);
}
export function arrayToUint8(array: number[]): Uint8Array {
return new Uint8Array(array);
return new Uint8Array(array);
}
+35 -35
View File
@@ -1,48 +1,48 @@
export const DATA_VERSION = "v2";
export function checkIsValidObject(object: any): boolean {
return object.dataVersion == DATA_VERSION;
return object.dataVersion == DATA_VERSION;
}
export function checkMatchesObjectStructure(
objectToCheck: any,
reference: any
objectToCheck: any,
reference: any,
): boolean {
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key]
);
if (doesNestedObjectMatch == false) return false;
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
}
return true;
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference,
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key],
);
if (doesNestedObjectMatch == false) return false;
}
}
return true;
}
export interface ValidObject {
dataVersion: "v2";
dataVersion: "v2";
}
+182 -181
View File
@@ -1,277 +1,278 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
export interface Stringifiable {
toString(): string;
toString(): string;
}
// crypto
export function generateRandomToken(length: number): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
}
// date
export function createTimestamp(): string {
return new Date().toISOString();
return new Date().toISOString();
}
// filters etc
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>(
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[]
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object)
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object),
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchReference(
reference: StringEntryObject,
stringEntryObject: StringEntryObject
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
): boolean {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined || stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
}
return true;
return true;
}
export function collectObjectValuesForKey<T>(
key: string,
converter: (object: T) => StringEntryObject,
objects: T[]
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
const values: Set<string> = new Set();
const values: Set<string> = new Set();
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
values.add(stringEntryObjectValue.toString());
}
values.add(stringEntryObjectValue.toString());
}
return [...values.values()];
return [...values.values()];
}
export function filterObjectsByWords<T>(
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[]
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object,
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchSearch<T>(
query: string,
getStringsOfObject: (object: T) => string[],
object: T
query: string,
getStringsOfObject: (object: T) => string[],
object: T,
): boolean {
if (query == "") return true;
if (query == "") return true;
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
}
return true;
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
}
}
return true;
}
// handlers
export type Handler<T> = (item: T) => void;
export class HandlerManager<T> {
handlers: Set<Handler<T>> = new Set();
handlers: Set<Handler<T>> = new Set();
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
}
// sorting
export class IndexManager<T> {
private itemToString: (item: T) => string;
private itemToString: (item: T) => string;
sortedStrings: string[] = [];
sortedStrings: string[] = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
}
// storage
export function getLocalStorageItemAndClear(key: string): string | null {
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
}
// string & parsing
export function stringify(data: any): string {
return JSON.stringify(data, null, 4);
return JSON.stringify(data, null, 4);
}
export function padZero(string: string | undefined, length: number): string {
return (string ?? "").padStart(length, "0");
return (string ?? "").padStart(length, "0");
}
export function parse(string: string): any {
try {
return JSON.parse(string);
} catch {
return {};
}
try {
return JSON.parse(string);
} catch {
return {};
}
}
export function parseValidObject<T>(string: string, reference: T): T | null {
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference
);
if (doesMatchReference == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference,
);
if (doesMatchReference == false) return null;
return parsed;
return parsed;
}
export function parseOrFallback(inputString: string): any {
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
}
export function parseArray(inputString: string): any[] {
const parsed: any = parseOrFallback(inputString);
if (Array.isArray(parsed) == false) return [];
return parsed;
const parsed: any = parseOrFallback(inputString);
if (Array.isArray(parsed) == false) return [];
return parsed;
}
// sort
export function localeCompare(a: string, b: string): number {
return a.localeCompare(b);
return a.localeCompare(b);
}