Skip to content
Merged
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
31 changes: 24 additions & 7 deletions apps/server/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,38 @@ export default defineNitroConfig({
fs.existsSync(pgliteLocalPath) ? pgliteLocalPath : pgliteRootPath,
);
const pgliteDistPath = path.join(pglitePkgPath, "dist");
const pgvectorLocalPath = path.resolve(
__dirname,
"node_modules/@electric-sql/pglite-pgvector/package.json",
);
const pgvectorRootPath = path.resolve(
__dirname,
"../../node_modules/@electric-sql/pglite-pgvector/package.json",
);
const pgvectorPkgPath = path.dirname(
fs.existsSync(pgvectorLocalPath) ? pgvectorLocalPath : pgvectorRootPath,
);

const assetsToCopy = ["pglite.data", "pglite.wasm"];
const assetsToCopy = [
{ name: "pglite.data", source: path.join(pgliteDistPath, "pglite.data") },
{ name: "pglite.wasm", source: path.join(pgliteDistPath, "pglite.wasm") },
{
name: "vector.tar.gz",
source: path.join(pgvectorPkgPath, "dist/vector.tar.gz"),
},
];

for (const asset of assetsToCopy) {
const source = path.join(pgliteDistPath, asset);
const destination = path.join(libsDir, asset);
const destination = path.join(libsDir, asset.name);

if (fs.existsSync(source)) {
if (fs.existsSync(asset.source)) {
if (!fs.existsSync(libsDir)) {
fs.mkdirSync(libsDir, { recursive: true });
}
fs.copyFileSync(source, destination);
console.log(`[Nitro] Successfully copied ${asset} to ${destination}`);
fs.copyFileSync(asset.source, destination);
console.log(`[Nitro] Successfully copied ${asset.name} to ${destination}`);
} else {
console.warn(`[Nitro] Warning: ${asset} not found at ${source}`);
console.warn(`[Nitro] Warning: ${asset.name} not found at ${asset.source}`);
}
}

Expand Down
61 changes: 60 additions & 1 deletion apps/server/scripts/isolated-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { copyFile, mkdir, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { defaultAppConfig } from "@solid-imager/core/domain/config/config-schema";
import { mediaGenerationInfo, medias, mediaSources } from "@solid-imager/db/schema";
Expand All @@ -16,6 +17,7 @@ import {
} from "../src/tests/e2e/support/fixture";

const appRoot = path.resolve(import.meta.dir, "..");
const E2E_PAGINATED_MEDIA_COUNT = 600;

export type IsolatedRuntime = {
routeTreePath: string;
Expand All @@ -42,8 +44,10 @@ function createImageSvg(accentColor: string, backgroundColor: string): Buffer {
async function seedMediaFixtures(runtimeDir: string): Promise<void> {
const mediaDir = path.join(runtimeDir, "media");
const thumbnailDir = path.join(runtimeDir, "thumbnails", E2E_SOURCE_ID);
const smallThumbnailDir = path.join(thumbnailDir, "256");
await mkdir(mediaDir, { recursive: true });
await mkdir(thumbnailDir, { recursive: true });
await mkdir(smallThumbnailDir, { recursive: true });

const primaryPath = path.join(mediaDir, E2E_PRIMARY_FILE_NAME);
const similarPath = path.join(mediaDir, E2E_SIMILAR_FILE_NAME);
Expand All @@ -64,6 +68,39 @@ async function seedMediaFixtures(runtimeDir: string): Promise<void> {
]);

const [primaryStats, similarStats] = await Promise.all([stat(primaryPath), stat(similarPath)]);
const paginatedMedia = Array.from({ length: E2E_PAGINATED_MEDIA_COUNT }, (_, index) => ({
id: randomUUID(),
fileName: `e2e-scroll-${String(index + 1).padStart(4, "0")}.png`,
}));
const primaryThumbnailPath = path.join(
thumbnailDir,
`${E2E_PRIMARY_MEDIA_ID}.webp`,
);
const similarThumbnailPath = path.join(
thumbnailDir,
`${E2E_SIMILAR_MEDIA_ID}.webp`,
);
await Promise.all(
[
copyFile(
primaryThumbnailPath,
path.join(smallThumbnailDir, `${E2E_PRIMARY_MEDIA_ID}.webp`),
),
copyFile(
similarThumbnailPath,
path.join(smallThumbnailDir, `${E2E_SIMILAR_MEDIA_ID}.webp`),
),
...paginatedMedia.flatMap(({ id, fileName }) => [
copyFile(primaryPath, path.join(mediaDir, fileName)),
copyFile(primaryThumbnailPath, path.join(thumbnailDir, `${id}.webp`)),
copyFile(
primaryThumbnailPath,
path.join(smallThumbnailDir, `${id}.webp`),
),
]),
],
);

const pgliteDir = path.join(runtimeDir, "pglite");
const client = createPglite(pgliteDir);
const db = drizzle(client);
Expand Down Expand Up @@ -111,6 +148,24 @@ async function seedMediaFixtures(runtimeDir: string): Promise<void> {
indexedAt: seededAt,
status: "active",
},
...paginatedMedia.map(({ id, fileName }, index) => {
const createdAt = new Date(seededAt.getTime() - (index + 2) * 1000);
return {
id,
mediaSourceId: E2E_SOURCE_ID,
filePath: fileName,
fileName,
mediaType: "image" as const,
width: 256,
height: 256,
fileSize: primaryStats.size,
description: "Paginated browser scroll fixture media",
createdAt,
modifiedAt: createdAt,
indexedAt: seededAt,
status: "active" as const,
};
}),
]);
await db.insert(mediaGenerationInfo).values([
{
Expand All @@ -121,6 +176,10 @@ async function seedMediaFixtures(runtimeDir: string): Promise<void> {
mediaId: E2E_SIMILAR_MEDIA_ID,
metadata: { fixture: "e2e-similar" },
},
...paginatedMedia.map(({ id }) => ({
mediaId: id,
metadata: { fixture: "e2e-scroll" },
})),
]);
} finally {
await client.close();
Expand Down
50 changes: 44 additions & 6 deletions apps/server/src/components/imports/pending-downloads-indicator.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,54 @@
import { subscribeToEventStream } from "@solid-imager/ui/event-stream";
import {
type ImportEventConnectedHandler,
type ImportEventHandler,
type PendingDownloadsIndicatorProps,
PendingDownloadsIndicator as SharedPendingDownloadsIndicator,
} from "@solid-imager/ui/pending-downloads-indicator";
import { orpc } from "~/infrastructure/api-clients/orpc-client";
import { fetchMediaSources } from "~/infrastructure/api-clients/sources-api";

type ImportSubscriber = {
handler: ImportEventHandler;
onConnected?: ImportEventConnectedHandler;
};

const importSubscribers = new Set<ImportSubscriber>();
let cleanupImportStream: (() => void) | undefined;

function subscribeSharedImportEvents(
handler: ImportEventHandler,
onConnected?: ImportEventConnectedHandler,
): () => void {
const subscriber = { handler, onConnected };
importSubscribers.add(subscriber);

if (!cleanupImportStream) {
cleanupImportStream = subscribeToEventStream(
(signal) => orpc.imports.events(undefined, { signal }),
(event) => {
for (const current of importSubscribers) {
void current.handler(event);
}
},
undefined,
async () => {
for (const current of importSubscribers) {
await current.onConnected?.();
}
},
);
}

return () => {
importSubscribers.delete(subscriber);
if (importSubscribers.size === 0) {
cleanupImportStream?.();
cleanupImportStream = undefined;
}
};
}

export function PendingDownloadsIndicator(
displayProps: { compact?: boolean; variant?: "default" | "v2" } = {},
) {
Expand Down Expand Up @@ -37,12 +80,7 @@ export function PendingDownloadsIndicator(
return { success: result.success };
},
subscribeImportEvents: (handler, onConnected) => {
return subscribeToEventStream(
(signal) => orpc.imports.events(undefined, { signal }),
handler,
undefined,
onConnected,
);
return subscribeSharedImportEvents(handler, onConnected);
},
};

Expand Down
13 changes: 4 additions & 9 deletions apps/server/src/components/v2/v2-app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
DialogHeader,
DialogTitle,
} from "@solid-imager/ui/dialog";
import { subscribeToEventStream } from "@solid-imager/ui/event-stream";
import type { RawEventHandler } from "@solid-imager/ui/hooks/use-sources-events";
import { useSourcesPage } from "@solid-imager/ui/hooks/use-sources-page";
import {
Expand Down Expand Up @@ -49,7 +48,7 @@ import { Link, useLocation } from "@tanstack/solid-router";
import type { JSX, ParentProps } from "solid-js";
import { createSignal, For, Show } from "solid-js";
import { PendingDownloadsIndicator } from "~/components/imports/pending-downloads-indicator";
import { orpc } from "~/infrastructure/api-clients/orpc-client";
import { createServerTransport } from "~/hooks/use-media-source-events";
import { mediaSourcesQueryOptions } from "~/infrastructure/api-clients/queries";
import {
createMediaSource,
Expand Down Expand Up @@ -80,13 +79,6 @@ const V2_NAVIGATION_ITEMS = [
{ icon: Settings, label: "Settings", to: "/v2/config" },
] as const;

function registerSourceEvents(handler: RawEventHandler): () => void {
return subscribeToEventStream(
(signal) => orpc.sources.events({ id: "*" }, { signal }),
handler,
);
}

function sourceTypeLabel(source: SafeMediaSource): string {
return source.type === "local" ? "Local" : source.type.toUpperCase();
}
Expand Down Expand Up @@ -357,6 +349,9 @@ function V2Sidebar(props: V2SidebarProps) {
export function V2AppShell(props: V2AppShellProps) {
const queryClient = useQueryClient();
const mediaSources = createQuery(mediaSourcesQueryOptions);
const sourceEventTransport = createServerTransport(() => "*");
const registerSourceEvents = (handler: RawEventHandler) =>
sourceEventTransport.listen(handler);
const [sidebarExpanded, setSidebarExpanded] = createSignal(true);
const [mobileMenuOpen, setMobileMenuOpen] = createSignal(false);
const sourceData = () => mediaSources.data ?? [];
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/hooks/use-media-source-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type SharedSourceTransport = {
idleSince: number | null;
};

const SOURCE_TRANSPORT_IDLE_TIMEOUT_MS = 30_000;
const SOURCE_TRANSPORT_IDLE_TIMEOUT_MS = 0;
const MAX_IDLE_SOURCE_TRANSPORTS = 2;

type SourceTransportGlobal = typeof globalThis & {
Expand Down Expand Up @@ -79,7 +79,7 @@ export function createServerTransport(
if (!id) {
return false;
}
if (pathname === "/search" || pathname === "/v2/search") {
if (id === "*" && (pathname === "/search" || pathname.startsWith("/v2/"))) {
return true;
}
if (id === "*") {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/routes/v2/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function V2SearchRoute() {
similarityTopK: () => searchState.similarityTopK,
refreshDebounceMs: SEARCH_RESULTS_REFRESH_DEBOUNCE_MS,
isSearchStateRestored,
scrollContainerSelector: "[data-media-scroll]",
scrollContainerSelector: '[data-media-scroll="v2-search"]',
});

useMediaSourceEvents(() => searchState.selectedSource || "*", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function MediaDetailHeader(props: {
returnPath.startsWith("/v2/sources/"))
) {
sessionStorage.removeItem("v2:media-return");
void navigate({ to: returnPath });
window.history.back();
return;
}
void navigate({
Expand Down
Loading