Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/docs/app/(diffshub)/(view)/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { redirect } from 'next/navigation';

import { ReviewUI } from '../_components/ReviewUI';
import { resolveDiffshubViewerRoute } from '../_components/utils';
import { loadInitialDiffshubPatchResponse } from '@/lib/diffshubPatchResponse';

export const dynamic = 'force-dynamic';

// Viewer route that mirrors the upstream path. GitHub is the public default,
// while hidden alternate domains can opt in through the `domain` query param.
Expand All @@ -19,11 +22,16 @@ export default async function DiffshubViewByPathPage({
if (route.kind === 'redirect') {
redirect(route.target);
}
const initialPatchResponse = loadInitialDiffshubPatchResponse({
domain: route.domain,
path: route.upstreamPath,
});

return (
<div className="flex h-dvh flex-col gap-2">
<ReviewUI
domain={route.domain}
initialPatchResponse={initialPatchResponse}
initialUrl={route.url}
path={route.upstreamPath}
/>
Expand Down
10 changes: 9 additions & 1 deletion apps/docs/app/(diffshub)/(view)/_components/ReviewUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ import {
removeSavedCommentSidebarEntry,
upsertSavedCommentSidebarEntry,
} from './utils';
import type { InitialDiffshubPatchResponse } from '@/lib/diffshubPatchTypes';

interface ReviewUIProps {
domain?: string;
initialPatchResponse: Promise<InitialDiffshubPatchResponse>;
initialUrl: string;
path: string;
}

export function ReviewUI({ domain, initialUrl, path }: ReviewUIProps) {
export function ReviewUI({
domain,
initialPatchResponse,
initialUrl,
path,
}: ReviewUIProps) {
useEffect(preloadAvatars, []);

const isWorkerPoolReadyOrDisable = useIsWorkerPoolReadyOrDisabled();
Expand Down Expand Up @@ -68,6 +75,7 @@ export function ReviewUI({ domain, initialUrl, path }: ReviewUIProps) {
} = usePatchLoader({
collapseMode,
domain,
initialPatchResponse,
onLoadStart: handlePatchLoadStart,
path,
viewerRef,
Expand Down
30 changes: 26 additions & 4 deletions apps/docs/app/(diffshub)/(view)/_components/streamGitPatchFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,34 @@ const GIT_FILE_BOUNDARY_SCAN_OVERLAP =
GIT_FILE_BOUNDARY_WITH_NEWLINE.length - 1;
const NON_WHITESPACE_PATTERN = /\S/;

export type GitPatchStream = ReadableStream<string | Uint8Array>;

export async function streamGitPatchFiles(
body: ReadableStream<Uint8Array>,
onFileText: (fileText: string) => Promise<void>
body: GitPatchStream,
onFileText: (fileText: string) => Promise<void>,
signal?: AbortSignal
): Promise<string | undefined> {
const reader = body.getReader();
const decoder = new TextDecoder();
const parser = createGitPatchFileStreamParser();
const abortReader = () => {
void reader.cancel().catch(() => {});
};
signal?.addEventListener('abort', abortReader, { once: true });

try {
for (;;) {
if (signal?.aborted === true) {
return undefined;
}

const result = await reader.read();
if (result.done) {
break;
}
if (result.value.byteLength > 0) {
parser.push(decoder.decode(result.value, { stream: true }));
const text = decodeGitPatchStreamChunk(decoder, result.value);
if (text.length > 0) {
parser.push(text);
await consumeAvailableStreamedFiles(parser, onFileText);
}
}
Expand All @@ -41,10 +53,20 @@ export async function streamGitPatchFiles(
}
return result.fallbackPatchContent;
} finally {
signal?.removeEventListener('abort', abortReader);
reader.releaseLock();
}
}

function decodeGitPatchStreamChunk(
decoder: TextDecoder,
chunk: string | Uint8Array
): string {
return typeof chunk === 'string'
? chunk
: decoder.decode(chunk, { stream: true });
}

export function getStreamedPatchMetadata(fileText: string): string | undefined {
const diffBoundaryIndex = findNextGitFileBoundary(fileText, 0);
if (diffBoundaryIndex == null || diffBoundaryIndex <= 0) {
Expand Down
64 changes: 57 additions & 7 deletions apps/docs/app/(diffshub)/(view)/_components/usePatchLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from './lineHash';
import {
getStreamedPatchMetadata,
type GitPatchStream,
streamGitPatchFiles,
} from './streamGitPatchFiles';
import type {
Expand All @@ -44,6 +45,7 @@ import type {
CommentMetadata,
ViewerLoadState,
} from './types';
import type { InitialDiffshubPatchResponse } from '@/lib/diffshubPatchTypes';

const STREAM_PUBLISH_INTERVAL_MS = 100;
const STREAM_INITIAL_PUBLISH_INTERVAL_MS = 500;
Expand All @@ -56,6 +58,7 @@ const GENERIC_PATCH_LOAD_ERROR_MESSAGE =
interface UsePatchLoaderOptions {
collapseMode: 'expanded' | 'collapsed';
domain?: string;
initialPatchResponse: Promise<InitialDiffshubPatchResponse>;
onLoadStart(): void;
path: string;
viewerRef: RefObject<CodeViewHandle<CommentMetadata> | null>;
Expand All @@ -80,6 +83,7 @@ interface UsePatchLoaderResult {
export function usePatchLoader({
collapseMode,
domain,
initialPatchResponse,
onLoadStart,
path,
viewerRef,
Expand Down Expand Up @@ -269,25 +273,28 @@ export function usePatchLoader({
}

console.time('-- request time');
const response = await fetch(`/api/diff?${patchSearchParams}`, {
cache: 'no-store',
signal: controller.signal,
});
const response =
loadAttempt === 0
? await initialPatchResponse
: await fetch(`/api/diff?${patchSearchParams}`, {
cache: 'no-store',
signal: controller.signal,
});
console.timeEnd('-- request time');

// This only catches route setup errors. GitHub fetch failures are
// delivered while consuming the stream so the UI can enter the
// streaming state as soon as the local transport opens.
if (!response.ok) {
const detail = (await response.text()).trim();
const detail = (await readPatchResponseText(response)).trim();
throw new Error(
detail.length > 0 ? detail : `Request failed (${response.status}).`
);
}

if (response.body == null) {
console.time('-- reading patch');
const patchContent = await response.text();
const patchContent = await readPatchResponseText(response);
console.timeEnd('-- reading patch');
await commitFullPatch(patchContent);
return;
Expand Down Expand Up @@ -402,6 +409,10 @@ export function usePatchLoader({
publishTreeSource();
};
const appendStreamedFile = async (fileText: string) => {
if (!isCurrentRequest()) {
return;
}

if (!hasReceivedFirstStreamedFile) {
hasReceivedFirstStreamedFile = true;
console.timeEnd('-- first streamed file');
Expand Down Expand Up @@ -454,7 +465,8 @@ export function usePatchLoader({
console.time('-- reading patch stream');
const fallbackPatchContent = await streamGitPatchFiles(
response.body,
appendStreamedFile
appendStreamedFile,
controller.signal
);
console.timeEnd('-- reading patch stream');
if (!isCurrentRequest()) {
Expand Down Expand Up @@ -488,6 +500,7 @@ export function usePatchLoader({
};
}, [
domain,
initialPatchResponse,
loadAttempt,
onLoadStart,
path,
Expand Down Expand Up @@ -576,6 +589,43 @@ function getNextItemVersion(item: { version?: string | number }): number {
return typeof item.version === 'number' ? item.version + 1 : 1;
}

type PatchResponseSource = Response | InitialDiffshubPatchResponse;

async function readPatchResponseText(
response: PatchResponseSource
): Promise<string> {
if ('bodyText' in response) {
if (response.bodyText != null) {
return response.bodyText;
}
return response.body == null ? '' : await readStreamText(response.body);
}

return response.text();
}

async function readStreamText(body: GitPatchStream): Promise<string> {
const reader = body.getReader();
const decoder = new TextDecoder();
let text = '';
try {
for (;;) {
const result = await reader.read();
if (result.done) {
break;
}
text +=
typeof result.value === 'string'
? result.value
: decoder.decode(result.value, { stream: true });
}
text += decoder.decode();
return text;
} finally {
reader.releaseLock();
}
}

function replaceLocationHash(hash: string | null): void {
const { pathname, search } = window.location;
const nextHash = hash ?? '';
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/app/(diffshub)/(view)/_components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function getPatchViewerHref(input: string): string | undefined {
const githubPath = getGitHubPathFromURL(parsedURL);
if (githubPath != null) return githubPath;
if (parsedURL.pathname !== '/') {
return `${parsedURL.pathname}?domain=${encodeURIComponent(parsedURL.hostname)}`;
return getAlternateDomainViewerHref(parsedURL);
}
return undefined;
} catch {
Expand All @@ -111,7 +111,7 @@ export function getPatchViewerHref(input: string): string | undefined {
const githubPath = getGitHubPathFromURL(parsedURL);
if (githubPath != null) return githubPath;
if (parsedURL.pathname !== '/') {
return `${parsedURL.pathname}?domain=${encodeURIComponent(parsedURL.hostname)}`;
return getAlternateDomainViewerHref(parsedURL);
}
} catch {
// Not parseable even with https:// prefix.
Expand Down Expand Up @@ -175,6 +175,10 @@ export function resolveDiffshubViewerRoute(
};
}

function getAlternateDomainViewerHref(parsedURL: URL): string {
return `${parsedURL.pathname}?domain=${encodeURIComponent(parsedURL.hostname)}`;
}

function getGitHubPathFromURL(parsedURL: URL): string | undefined {
if (parsedURL.hostname === GITHUB_HOST) {
if (parsedURL.pathname === '/') {
Expand Down
Loading
Loading