From fc845f2661be61b1a86501eed306cb0d7cb60d73 Mon Sep 17 00:00:00 2001 From: Tom van der Lee Date: Tue, 18 Jan 2022 22:03:20 +0100 Subject: Show historic requests for the session on page load --- index.html | 2 +- src/components/App/App.tsx | 46 +++++++++++++++++++++++-------- src/hooks/useRequests.tsx | 69 ++++++++++++++++++++++++++++++++++++++-------- ttun/__main__.py | 16 +++++++++-- ttun/inspect_server.py | 19 +++++++------ ttun/pubsub.py | 21 ++++++++++++-- 6 files changed, 136 insertions(+), 37 deletions(-) diff --git a/index.html b/index.html index a09a4ed..4e51724 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ - React Vite Micro Typescript App +
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index 3a7fe9b..e361aa5 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import useRequests, {RequestResponse} from "../../hooks/useRequests"; +import useRequests, {RequestResponse, ReadyState} from "../../hooks/useRequests"; import {useEffect, useMemo, useState} from "react"; import styles from './App.module.scss'; @@ -11,34 +11,56 @@ interface Config { url: string } +type ReadyStateMap = { + [ReadyState.CONNECTING]: string, + [ReadyState.OPEN]: string, + [ReadyState.CLOSING]: string, + [ReadyState.CLOSED]: string, +} + +const statusMap: ReadyStateMap = { + [ReadyState.CONNECTING]: '🔴', + [ReadyState.OPEN]: '🟢', + [ReadyState.CLOSING]: '🔴', + [ReadyState.CLOSED]: '🔴', +} + export default function App() { + const [config, setConfig]= useState(null) + + const { calls, readyState } = useRequests({ + onConnect: async () => { + const response = await fetch(`http://${getHost()}/config/`) + const config = await response.json() + setConfig(config) + } + }); + useEffect(() => { - fetch(`http://${getHost()}/config/`) - .then(response => response.json() as Promise) - .then(setConfig) - }, []) + const url = new URL(config?.url ?? 'https://loading...'); + document.title = `${statusMap[readyState]} ${url.host} | TTUN`; + }, [readyState, config?.url]) - const requests = useRequests(); const [selectedRequestIndex, setSelectedRequestIndex] = useState(null); const selectedRequest = useMemo(() => ( selectedRequestIndex === null ? null - : requests[selectedRequestIndex] - ), [selectedRequestIndex, requests]); + : calls[selectedRequestIndex] + ), [selectedRequestIndex, calls]); return config && (
- TTUN + {statusMap[readyState]} TTUN {config.url}
    { - requests.length > 0 - ? requests.slice(0).reverse().map((requestResponse, index) => ( -
  • setSelectedRequestIndex(requests.length - index - 1)} key={`request-${index}`}> + calls.length > 0 + ? calls.slice(0).reverse().map((requestResponse, index) => ( +
  • setSelectedRequestIndex(calls.length - index - 1)} key={`request-${index}`}>
  • )) diff --git a/src/hooks/useRequests.tsx b/src/hooks/useRequests.tsx index 3ad5cc7..2b8393e 100644 --- a/src/hooks/useRequests.tsx +++ b/src/hooks/useRequests.tsx @@ -33,25 +33,67 @@ interface Response { payload: ResponsePayload, } +interface Historic { + type: 'historic' + payload: (Request | Response)[] +} + export interface RequestResponse { request: RequestPayload response?: ResponsePayload } -export default function useRequests(): RequestResponse[] { +export enum ReadyState { + CONNECTING = 0, + OPEN = 1, + CLOSING = 2, + CLOSED = 3, +} + +export interface useRequestsProps { + onConnect: () => Promise +} + +export interface UseRequests { + calls: RequestResponse[] + readyState: ReadyState +} + +export default function useRequests({ onConnect }: useRequestsProps): UseRequests { const wsHost = useMemo(getHost, []); - const connect = useCallback(() => new WebSocket(`ws://${wsHost}/inspect/`), [wsHost]) const [requests, setRequests] = useState([]); const [responses, setResponses] = useState([]); - const [ws, setWs] = useState(() => connect()); + + + const connect = useCallback(() => ( + new WebSocket(`ws://${wsHost}/inspect/`) + ), [wsHost]); + + const [ws, setWs] = useState(() => connect()); + const [readyState, setReadyState] = useState(ws.readyState); useEffect(() => { - const reconnect = () => setWs(() => connect()) + setReadyState(ws.readyState); + + const onClose = () => { + setReadyState(ws.readyState); + setWs(connect()); + } + const onOpen = () => { + onConnect(); + setReadyState(ws.readyState); + } const onMessage = ({ data }) => { - const { type, payload } = JSON.parse(data) as Request | Response + const { type, payload } = JSON.parse(data) as Historic | Request | Response switch (type) { + case 'historic': + const requests = (payload as (Request | Response)[]).filter(({ type }) => type === 'request'); + const responses = (payload as (Request | Response)[]).filter(({ type }) => type === 'response'); + setRequests((rqs) => [...rqs, ...requests.map(({ payload }) => payload as RequestPayload)]); + setResponses((rps) => [...rps, ...responses.map(({ payload }) => payload as ResponsePayload)]); + break case 'request': setRequests((rqs) => [...rqs, payload as RequestPayload]) break @@ -62,16 +104,21 @@ export default function useRequests(): RequestResponse[] { } ws.addEventListener('message', onMessage) - ws.addEventListener('close', reconnect) + ws.addEventListener('close', onClose) + ws.addEventListener('open', onOpen) return () => { ws.removeEventListener('message', onMessage); - ws.removeEventListener('close', reconnect); + ws.removeEventListener('close', onClose); + ws.removeEventListener('open', onOpen) } }, [ws]) - return useMemo(() => requests.map((request) => ({ - request: request, - response: responses.find(({ id }) => id === request.id) - })), [requests, responses]) + return { + calls: useMemo(() => requests.map((request) => ({ + request: request, + response: responses.find(({id}) => id === request.id) + })), [requests, responses]), + readyState, + } } diff --git a/ttun/__main__.py b/ttun/__main__.py index 1a3ca69..52bf2fb 100644 --- a/ttun/__main__.py +++ b/ttun/__main__.py @@ -42,13 +42,25 @@ def main(): loop.run_until_complete(client.connect()) - server = Server(config=client.config, on_resend=client.proxyRequest) + def print_info(server: Server): + print('Tunnel created:') + print(f'{client.config["url"]} -> http://localhost:{args.port}') + print('') + print(f'Inspect requests:') + print(f'http://localhost:{server.port}') + + server = Server( + config=client.config, + on_resend=client.proxyRequest, + on_started=print_info, + ) + tasks = { loop.create_task(client.handle_messages()), loop.create_task(server.run()) } - print(client.config['url']) + try: loop.run_until_complete(asyncio.wait(tasks, return_when=FIRST_EXCEPTION)) except (CancelledError, TimeoutError): diff --git a/ttun/inspect_server.py b/ttun/inspect_server.py index 37f1aea..ec5b58d 100644 --- a/ttun/inspect_server.py +++ b/ttun/inspect_server.py @@ -10,18 +10,17 @@ from ttun.types import Config, RequestData BASE_DIR = Path(__file__).resolve().parent -def no_print(*args, **kwargs): - pass - - class Server: - def __init__(self, config: Config, on_resend: Callable[[RequestData], Awaitable]): + def __init__(self, config: Config, on_resend: Callable[[RequestData], Awaitable], on_started: Callable[['Server'], None]): + self.port = 4040 + self.config = { **config, 'assets': '/assets/' } self.on_resend = on_resend + self.on_started = lambda *args, **kwargs: on_started(self) self.app = web.Application() with resources.path(__package__, 'staticfiles') as staticfiles: @@ -34,12 +33,11 @@ class Server: ]) async def run(self): - port = 4040 while True: try: - await web._run_app(self.app, port=port) + await web._run_app(self.app, port=self.port, print=self.on_started) except OSError: - port += 1 + self.port += 1 async def root(self, request: web.Request): with resources.path(__package__, 'staticfiles') as staticfiles: @@ -64,6 +62,11 @@ class Server: ws = web.WebSocketResponse() await ws.prepare(request) + await ws.send_json({ + 'type': 'historic', + 'payload': PubSub.history + }) + with PubSub.subscribe() as subscription: while message := await subscription.get(): await ws.send_json(message, False) diff --git a/ttun/pubsub.py b/ttun/pubsub.py index 5d155ea..64dd2b8 100644 --- a/ttun/pubsub.py +++ b/ttun/pubsub.py @@ -8,6 +8,7 @@ class PubSub: def __init__(self): self.queues: list[asyncio.Queue] = [] + self._history= [] @classmethod def instance(cls) -> 'PubSub': @@ -26,10 +27,24 @@ class PubSub: try: yield queue finally: - instance.queues.remove(queue) - del queue + cls.unsubscribe(queue) + + @classmethod + def unsubscribe(cls, queue: asyncio.Queue): + instance = cls.instance() + instance.queues.remove(queue) + del queue + + @classmethod + @property + def history(cls): + instance = cls.instance() + return instance._history @classmethod async def publish(cls, msg: Any): - for queue in cls.instance().queues: + instance = cls.instance() + + instance._history.append(msg) + for queue in instance.queues: await queue.put(msg) -- cgit v1.2.3