Просмотр исходного кода

show all routes on the links page

Kelly Norton 11 месяцев назад
Родитель
Сommit
a9f6ee86fc
5 измененных файлов с 87 добавлено и 34 удалено
  1. 13 5
      ui/src/LinksPage/RoutesContext/RoutesProvider.tsx
  2. 21 5
      ui/src/api.ts
  3. 3 3
      ui/src/links.main.tsx
  4. 25 0
      ui/src/restartable.ts
  5. 25 21
      ui/src/result.ts

+ 13 - 5
ui/src/LinksPage/RoutesContext/RoutesProvider.tsx

@@ -1,17 +1,25 @@
 import { ReactNode, useEffect, useMemo, useState } from "react";
 import { RoutesContext } from "./RoutesContext";
 import { Result } from "../../result";
-import { Route, apiErrorToString, getRoutes } from "../../api";
+import { Route, apiErrorToString, getAllRoutes } from "../../api";
+import { restartableAsync } from "../../restartable";
 
 export const RoutesProvider = ({ children }: { children: ReactNode }) => {
   const [result, setResult] = useState<Result<Route[]>>(Result.of([]));
 
-  const res = useMemo(() => {
-    return Result.from(() => getRoutes(), [], apiErrorToString);
-  }, []);
+  const res = useMemo(() => restartableAsync(getAllRoutes()), []);
 
   useEffect(() => {
-    res.then(setResult);
+    const fetchAllRoutes = async () => {
+      const allRoutes = [];
+      for await (const routes of res()) {
+        allRoutes.push(...routes);
+        setResult(Result.of(allRoutes));
+      }
+    };
+    fetchAllRoutes().catch((e) =>
+      setResult(Result.error([], apiErrorToString(e)))
+    );
   }, [res, setResult]);
 
   return (

+ 21 - 5
ui/src/api.ts

@@ -30,6 +30,7 @@ interface RoutesResponse {
   ok: boolean;
   error?: string;
   routes?: RawRoute[];
+  next: string;
 }
 
 async function fromResponse<T extends { ok: boolean; error?: string }, V>(
@@ -71,12 +72,27 @@ export async function getConfig(): Promise<Config> {
   return host === "" ? { host: location.host } : { host };
 }
 
-export async function getRoutes(): Promise<Route[]> {
-  const routes = await fromResponse(
-    await fetch("/api/urls/"),
-    (data: RoutesResponse) => data.routes?.map(toRoute) ?? []
+export async function getRoutes(
+  next: string,
+  limit: number = 1000
+): Promise<[Route[], string]> {
+  const value = await fromResponse(
+    await fetch(`/api/urls/?cursor=${next}&limit=${limit}`),
+    (data: RoutesResponse) =>
+      [data.routes?.map(toRoute) ?? [], data.next] as [Route[], string]
   );
-  return routes ?? [];
+  return value ?? [[], next];
+}
+
+export async function* getAllRoutes(
+  pageSize: number = 1000
+): AsyncGenerator<Route[]> {
+  let cursor = "";
+  do {
+    const [routes, next] = await getRoutes(cursor, pageSize);
+    yield routes;
+    cursor = next;
+  } while (cursor !== "");
 }
 
 export async function postRoute(name: string, url: string): Promise<Route> {

+ 3 - 3
ui/src/links.main.tsx

@@ -5,7 +5,7 @@ import { LinksPage } from "./LinksPage";
 import "./links.main.scss";
 
 createRoot(document.getElementById("root")!).render(
-	<StrictMode>
-		<LinksPage />
-	</StrictMode>
+  <StrictMode>
+    <LinksPage />
+  </StrictMode>
 );

+ 25 - 0
ui/src/restartable.ts

@@ -0,0 +1,25 @@
+export function restartableAsync<T>(
+  iter: AsyncIterable<T>
+): () => AsyncIterable<T> {
+  // buffer stores all items that have been previously consumed.
+  const buffer: T[] = [];
+  return async function* () {
+    // index of the next item in the buffer to yield.
+    let i = 0;
+    // produce all items previously consumed by other iterators.
+    for (; i < buffer.length; i++) {
+      yield buffer[i];
+    }
+    // now takes the next from the iterator.
+    for await (const item of iter) {
+      // this is a little subtle, but other concurrent iterators may have
+      // consumed and buffered items while we were waiting. So we need to put
+      // our new item in the back of the buffer and yield from where we preiously
+      // left off.
+      buffer.push(item);
+      for (; i < buffer.length; i++) {
+        yield buffer[i];
+      }
+    }
+  };
+}

+ 25 - 21
ui/src/result.ts

@@ -1,31 +1,35 @@
 export interface Result<T> {
-	value: T;
-	error: string;
+  value: T;
+  error: string;
 }
 
 export const defaultErrorToString = (e: unknown): string => {
-	if (typeof e === 'string') {
-		return e;
-	} else if (e instanceof Error) {
-		return e.message;
-	}
-	return 'An unknown error occurred';
-}
+  if (typeof e === "string") {
+    return e;
+  } else if (e instanceof Error) {
+    return e.message;
+  }
+  return "An unknown error occurred";
+};
 
 export const of = <T>(value: T): Result<T> => {
-	return { value, error: '' };
-}
+  return { value, error: "" };
+};
+
+export const error = <T>(value: T, error: string): Result<T> => {
+  return { value, error };
+};
 
 export const from = async <T>(
-	op: () => Promise<T>,
-	defaultValue: T,
-	errorToString: (e: unknown) => string = defaultErrorToString,
+  op: () => Promise<T>,
+  defaultValue: T,
+  errorToString: (e: unknown) => string = defaultErrorToString
 ): Promise<Result<T>> => {
-	try {
-		return of(await op());
-	} catch (e) {
-		return { value: defaultValue, error: errorToString(e) };
-	}
-}
+  try {
+    return of(await op());
+  } catch (e) {
+    return { value: defaultValue, error: errorToString(e) };
+  }
+};
 
-export const Result = { of, from };
+export const Result = { of, from, error };