Big restructure
This commit is contained in:
parent
1d0ebeb2b7
commit
4ee57ab459
69 changed files with 124 additions and 124 deletions
30
packages/ttp/package.json
Normal file
30
packages/ttp/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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": {
|
||||
"@tanstack/react-router": "^1.0.0",
|
||||
"@tensamin/crypto": "workspace:*",
|
||||
"@tensamin/storage": "workspace:*",
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ui": "workspace:*",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^10.0.3"
|
||||
}
|
||||
}
|
||||
237
packages/ttp/src/context.tsx
Normal file
237
packages/ttp/src/context.tsx
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import * as React from "react";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
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 Loading from "@tensamin/ui/screens/loading";
|
||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||
|
||||
type ContextType = {
|
||||
send: BoundSendFn<Schemas>;
|
||||
readyState: () => number;
|
||||
ownPing: () => number;
|
||||
iotaPing: () => number;
|
||||
};
|
||||
|
||||
const socketContext = React.createContext<ContextType | undefined>(undefined);
|
||||
|
||||
export default function Provider(props: { children: React.ReactNode }) {
|
||||
const [readyState, setReadyState] = React.useState<number>(READY_STATE.CLOSED);
|
||||
const [connected, setConnected] = React.useState<boolean>(false);
|
||||
|
||||
const [ownPing, setOwnPing] = React.useState<number>(0);
|
||||
const [iotaPing, setIotaPing] = React.useState<number>(0);
|
||||
|
||||
const [error, setError] = React.useState("");
|
||||
const [errorDescription, setErrorDescription] = React.useState("");
|
||||
|
||||
const clientRef = React.useRef<ReturnType<
|
||||
typeof createTransportClient<Schemas>
|
||||
> | null>(null);
|
||||
|
||||
const send = React.useCallback<BoundSendFn<Schemas>>(
|
||||
((
|
||||
type: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: { id?: number; noResponse?: boolean },
|
||||
) => {
|
||||
const client = clientRef.current;
|
||||
|
||||
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>,
|
||||
[],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const originalNow = Date.now();
|
||||
|
||||
const data = await send("ping", {
|
||||
last_ping: originalNow,
|
||||
});
|
||||
|
||||
const travelTime = Date.now() - originalNow;
|
||||
setOwnPing(travelTime);
|
||||
|
||||
const remotePing = data.data.ping_iota;
|
||||
if (typeof remotePing === "number") {
|
||||
setIotaPing(remotePing);
|
||||
}
|
||||
} catch (intervalError) {
|
||||
log(1, "Socket", "yellow", "Ping failed", intervalError);
|
||||
}
|
||||
}, PING_INTERVAL);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [connected, send]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let attempts = 0;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let reconnectScheduled = false;
|
||||
let disposed = false;
|
||||
|
||||
const clearReconnectTimer = () => {
|
||||
if (!reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
reconnectScheduled = false;
|
||||
};
|
||||
|
||||
const scheduleReconnect = (reason?: unknown) => {
|
||||
if (disposed || reconnectScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts >= RETRY_COUNT) {
|
||||
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", reason);
|
||||
return;
|
||||
}
|
||||
|
||||
attempts += 1;
|
||||
reconnectScheduled = true;
|
||||
reconnectTimer = setTimeout(() => {
|
||||
reconnectScheduled = false;
|
||||
reconnectTimer = null;
|
||||
void connect();
|
||||
}, RETRY_INTERVAL);
|
||||
};
|
||||
|
||||
const transportClient = createTransportClient(schemas, {
|
||||
url: TRANSPORT_URL,
|
||||
onReadyStateChange: (state) => {
|
||||
setReadyState(state);
|
||||
|
||||
if (state === READY_STATE.OPEN) {
|
||||
attempts = 0;
|
||||
clearReconnectTimer();
|
||||
setConnected(true);
|
||||
setError("");
|
||||
setErrorDescription("");
|
||||
log(1, "Socket", "green", "Connected");
|
||||
return;
|
||||
}
|
||||
|
||||
setConnected(false);
|
||||
},
|
||||
onClose: ({ error: closeError, intentional }) => {
|
||||
setConnected(false);
|
||||
|
||||
if (disposed || intentional) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(0, "Socket", "red", "Disconnected", closeError);
|
||||
scheduleReconnect(closeError);
|
||||
},
|
||||
});
|
||||
|
||||
clientRef.current = transportClient;
|
||||
|
||||
async function connect() {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await transportClient.connect(TRANSPORT_URL);
|
||||
} catch (connectError) {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
||||
scheduleReconnect(connectError);
|
||||
}
|
||||
}
|
||||
|
||||
void connect();
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
clearReconnectTimer();
|
||||
|
||||
if (clientRef.current === transportClient) {
|
||||
clientRef.current = null;
|
||||
}
|
||||
|
||||
void transportClient.close("context-dispose");
|
||||
setReadyState(READY_STATE.CLOSED);
|
||||
setConnected(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const progress = React.useMemo(() => {
|
||||
if (readyState === READY_STATE.CONNECTING) return 70;
|
||||
if (!connected) return 90;
|
||||
return 100;
|
||||
}, [connected, readyState]);
|
||||
|
||||
const contextValue = React.useMemo<ContextType>(
|
||||
() => ({
|
||||
send,
|
||||
readyState: () => readyState,
|
||||
ownPing: () => ownPing,
|
||||
iotaPing: () => iotaPing,
|
||||
}),
|
||||
[iotaPing, ownPing, readyState, send],
|
||||
);
|
||||
|
||||
if (error !== "" && errorDescription !== "") {
|
||||
return <ErrorScreen error={error} description={errorDescription} />;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
return <Loading progress={progress} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<socketContext.Provider value={contextValue}>
|
||||
{props.children}
|
||||
</socketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSocket(): ContextType {
|
||||
const context = React.useContext(socketContext);
|
||||
if (!context) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
1533
packages/ttp/src/core.ts
Normal file
1533
packages/ttp/src/core.ts
Normal file
File diff suppressed because it is too large
Load diff
5
packages/ttp/src/values.ts
Normal file
5
packages/ttp/src/values.ts
Normal 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";
|
||||
12
packages/ttp/tsconfig.json
Normal file
12
packages/ttp/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Loading…
Reference in a new issue