Moved TTP to this repo and updated other stuff

This commit is contained in:
Alois 2026-03-10 21:04:09 +01:00
commit 38241e57eb
14 changed files with 1823 additions and 71 deletions

View file

@ -19,7 +19,7 @@
"@tanstack/solid-query": "^5.90.23",
"@tanstack/solid-virtual": "^3.13.21",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/ttp": "workspace:*",
"@tensamin/core-storage": "workspace:*",
"@tensamin/core-user": "workspace:*",
"@tensamin/markdown": "workspace:*",

View file

@ -16,7 +16,7 @@
"@codemirror/commands": "^6.10.2",
"@codemirror/lang-markdown": "^6.5.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.16",
"@codemirror/view": "^6.39.15",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.11"
}

View file

@ -0,0 +1,29 @@
{
"name": "@tensamin/ttp",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./core": "./src/core.ts",
"./context": "./src/context.tsx",
"./send": "./src/send.ts",
"./values": "./src/values.ts"
},
"scripts": {
"format": "bunx prettier --write .",
"lint": "eslint src --ext .ts,.tsx",
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@solidjs/router": "^0.15.4",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/core-storage": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "workspace:*",
"solid-js": "^1.9.10",
"zod": "^4.3.6"
},
"devDependencies": {
"eslint": "^10.0.3"
}
}

View file

@ -0,0 +1,312 @@
import { log } from "@tensamin/shared/log";
import {
createContext,
createEffect,
createSignal,
onCleanup,
Show,
untrack,
useContext,
type ParentProps,
} from "solid-js";
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
import {
PING_INTERVAL,
RETRY_COUNT,
RETRY_INTERVAL,
TRANSPORT_URL,
} from "./values";
import {
socket as schemas,
type Socket as Schemas,
} from "@tensamin/shared/data";
import { useStorage } from "@tensamin/core-storage/context";
import { useCrypto } from "@tensamin/core-crypto/context";
import Loading from "@tensamin/ui/screens/loading";
import ErrorScreen from "@tensamin/ui/screens/error";
import { useNavigate } from "@solidjs/router";
type OmikronData = {
id: number;
public_key: string;
ip_address: string;
};
type ContextType = {
send: BoundSendFn<Schemas>;
readyState: () => number;
ownPing: () => number;
iotaPing: () => number;
};
const socketContext = createContext<ContextType>();
export default function Provider(props: ParentProps) {
const [omikron, setOmikron] = createSignal<OmikronData | null>(null);
const [readyState, setReadyState] = createSignal<number>(READY_STATE.CLOSED);
const [identified, setIdentified] = createSignal<boolean>(false);
const [ownPing, setOwnPing] = createSignal<number>(0);
const [iotaPing, setIotaPing] = createSignal<number>(0);
const [error, setError] = createSignal<string>("");
const [errorDescription, setErrorDescription] = createSignal<string>("");
const { load } = useStorage();
const { get_shared_secret, decrypt } = useCrypto();
const navigate = useNavigate();
let client: ReturnType<typeof createTransportClient<Schemas>> | null = null;
// Load Omikron
createEffect(() => {
if (omikron()) return;
const controller = new AbortController();
(async () => {
try {
const userId = await load("user_id");
// Redirect to login
if (userId === 0) {
navigate("/login");
return;
}
const res = await fetch(
"https://omega.tensamin.net/api/get/omikron/" + String(userId),
{ signal: controller.signal },
);
const data = await res.json();
setOmikron(data);
} catch (e) {
if (controller.signal.aborted) return;
setError("Failed to load Omikron data");
setErrorDescription(
"An error occurred while fetching the Omikron server data. Please try again later.",
);
log(0, "Socket", "red", "Failed to fetch Omikron data", e);
}
})();
onCleanup(() => controller.abort());
});
createEffect(() => {
if (identified()) {
const interval = setInterval(async () => {
try {
const originalNow = Date.now();
const data = await send("ping", {
last_ping: originalNow,
});
const travelTime = Date.now() - originalNow;
setOwnPing(travelTime);
setIotaPing(data.data.ping_iota);
} catch (error) {
log(1, "Socket", "yellow", "Ping failed", error);
}
}, PING_INTERVAL);
onCleanup(() => clearInterval(interval));
}
});
// Create connection
createEffect(() => {
if (!omikron()) return;
let attempts = 0;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let disposed = false;
const transportClient = createTransportClient(schemas, {
url: TRANSPORT_URL,
onReadyStateChange: setReadyState,
onClose: ({ error: closeError, intentional }) => {
setIdentified(false);
if (disposed || intentional) {
return;
}
log(0, "Socket", "red", "Disconnected", closeError);
if (attempts < RETRY_COUNT) {
attempts += 1;
reconnectTimer = setTimeout(() => {
void connect();
}, RETRY_INTERVAL);
return;
}
setError("Connection Failed");
setErrorDescription(
"Unable to connect to the server after multiple attempts. Please check your internet connection or try again later.",
);
log(0, "Socket", "red", "Reconnection attempts exhausted", closeError);
},
});
client = transportClient;
async function identify(activeClient: typeof transportClient) {
const userId = await load("user_id");
const privateKey = await load("private_key");
const currentOmikron = untrack(() => omikron());
activeClient
.send("identification", { user_id: userId })
.then(async (data) => {
const ownUserData = await activeClient.send("get_user_data", {
user_id: userId,
});
if (!currentOmikron?.public_key) {
setError("Omikron data missing");
setErrorDescription(
"Omikron server data is missing. Please try again later.",
);
log(0, "Socket", "red", "Omikron public key missing");
return;
}
try {
const sharedSecret = await get_shared_secret(
privateKey,
ownUserData.data.public_key,
currentOmikron.public_key,
);
const solvedChallenge = await decrypt(
sharedSecret,
data.data.challenge,
);
activeClient
.send("challenge_response", {
challenge: btoa(solvedChallenge),
})
.then(() => {
if (client !== activeClient || disposed) {
return;
}
log(1, "Socket", "green", "Identification successful");
setIdentified(true);
setError("");
setErrorDescription("");
})
.catch((error) => {
log(0, "Socket", "red", "Challenge failed", error);
setError("Challenge Failed");
setErrorDescription(
"Failed to respond to the server's challenge. Please try again later.",
);
});
} catch (err) {
setError("Validation Failed");
setErrorDescription(
"Failed to validate the server's identity. Please try again later.",
);
log(0, "Socket", "red", "Server identity validation failed", err);
}
})
.catch((e) => {
log(0, "Socket", "red", "Identification failed", e);
setError("Identification Failed");
setErrorDescription(
"Failed to identify with the server. Please try again later.",
);
});
}
async function connect() {
if (disposed) return;
try {
await transportClient.connect(TRANSPORT_URL);
attempts = 0;
await identify(transportClient);
} catch (error) {
if (!disposed) {
log(0, "Socket", "red", "Connection attempt failed", error);
}
}
}
void connect();
onCleanup(() => {
disposed = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
if (client === transportClient) {
client = null;
}
void transportClient.close("context-dispose");
setReadyState(READY_STATE.CLOSED);
setIdentified(false);
});
});
// Create Send Function
const send: BoundSendFn<Schemas> = ((
type: string,
data?: Record<string, unknown>,
options?: { id?: number; noResponse?: boolean },
) => {
if (!client) {
return Promise.reject(new Error("Socket is not connected"));
}
if (options?.noResponse) {
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: true,
});
}
return client.send(type as keyof Schemas & string, data as never, {
...options,
noResponse: false,
});
}) as BoundSendFn<Schemas>;
const progress = () => {
if (!omikron()) return 40;
if (readyState() !== READY_STATE.OPEN) return 70;
if (!identified()) return 90;
return 100;
};
return (
<Show
when={error() === "" && errorDescription() === ""}
fallback={
<ErrorScreen error={error()} description={errorDescription()} />
}
>
<Show
when={omikron() && identified()}
fallback={<Loading progress={progress()} />}
>
<socketContext.Provider value={{ send, readyState, ownPing, iotaPing }}>
{props.children}
</socketContext.Provider>
</Show>
</Show>
);
}
export function useSocket(): ContextType {
const context = useContext(socketContext);
if (!context)
throw new Error("useSocket must be used within a SocketProvider");
return context;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
export const RESPONSE_TIMEOUT = 15_000;
export const RETRY_COUNT = 10;
export const RETRY_INTERVAL = 3_000;
export const PING_INTERVAL = 5_000;
export const TRANSPORT_URL = "https://methanium.net:959";

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"strict": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}

View file

@ -14,7 +14,7 @@
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/ttp": "workspace:*",
"@tensamin/shared": "workspace:*",
"solid-js": "^1.9.11",
"zod": "^4.3.6"

View file

@ -13,7 +13,7 @@
"dependencies": {
"@tensamin/chat": "workspace:*",
"@tensamin/core-crypto": "workspace:*",
"@tensamin/ttp": "git+https://github.com/Tensamin/TTP.git",
"@tensamin/ttp": "workspace:*",
"@tensamin/core-storage": "workspace:*",
"@tensamin/core-user": "workspace:*",
"@tensamin/shared": "workspace:*",