Compare commits

...

8 Commits

Author SHA1 Message Date
Matt Aitken 1655ff0459 Runs table powered by Electric, no filtering yet 2024-11-19 21:12:33 +00:00
Matt Aitken f779a097ef Syncing ElectricSQL to PGLite is working 2024-11-19 20:10:25 +00:00
Matt Aitken e67e86b63e Got the PgProvider working 2024-11-19 18:36:50 +00:00
Matt Aitken 52d5fff4f2 WIP getting PGlite working 2024-11-19 16:43:28 +00:00
Matt Aitken 13bfc045da WIP trying to get it to work… but failing 2024-11-19 15:28:24 +00:00
Matt Aitken 94225426c3 pglite-react 2024-11-19 15:16:31 +00:00
Matt Aitken 0c88231bf7 Added some hidden routes to try PGLite out on 2024-11-19 12:09:43 +00:00
Matt Aitken fb14f43264 Initial experiments getting pglite working 2024-11-19 12:09:27 +00:00
14 changed files with 791 additions and 3 deletions
@@ -105,6 +105,35 @@ type RunFiltersProps = {
hasFilters: boolean;
};
export function useRunFilters() {
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const filters = {
cursor: searchParams.get("cursor") ?? undefined,
direction: searchParams.get("direction") ?? undefined,
statuses: searchParams.getAll("statuses") as TaskRunStatus[],
environments: searchParams.getAll("environments"),
tasks: searchParams.getAll("tasks"),
period: searchParams.get("period") ?? undefined,
bulkId: searchParams.get("bulkId") ?? undefined,
tags: searchParams.getAll("tags").map((t) => decodeURIComponent(t)),
versions: searchParams.getAll("versions"),
from: searchParams.get("from") ? parseInt(searchParams.get("from")!) : undefined,
to: searchParams.get("to") ? parseInt(searchParams.get("to")!) : undefined,
};
const hasFilters =
searchParams.has("statuses") ||
searchParams.has("environments") ||
searchParams.has("tasks") ||
searchParams.has("period") ||
searchParams.has("bulkId") ||
searchParams.has("tags");
return { filters, hasFilters };
}
export function RunsFilters(props: RunFiltersProps) {
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
+116
View File
@@ -0,0 +1,116 @@
import { PGlite } from "@electric-sql/pglite";
import { electricSync } from "@electric-sql/pglite-sync";
import { live } from "@electric-sql/pglite/live";
export type PGClient = Awaited<ReturnType<typeof createClient>>;
export async function createClient() {
const dataDir = "idb://triggerdotdev";
// Fetch both files
const [wasmResponse, dataResponse] = await Promise.all([
fetch("/wasm/postgres.wasm"),
fetch("/wasm/postgres.data"),
]);
// Convert to appropriate formats
const wasmBuffer = await wasmResponse.arrayBuffer();
const wasmModule = await WebAssembly.compile(wasmBuffer);
const fsBundle = new Blob([await dataResponse.arrayBuffer()]);
const db = await PGlite.create({
dataDir,
extensions: { live, electric: electricSync() },
wasmModule,
fsBundle, // Provide the data file as a blob
// debug: 5,
});
//TaskRunStatus
await db
.exec(
`
CREATE TYPE "public"."TaskRunStatus" AS ENUM (
'PENDING', 'EXECUTING', 'WAITING_TO_RESUME', 'RETRYING_AFTER_FAILURE',
'PAUSED', 'CANCELED', 'COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS',
'INTERRUPTED', 'SYSTEM_FAILURE', 'CRASHED', 'WAITING_FOR_DEPLOY',
'DELAYED', 'EXPIRED', 'TIMED_OUT'
);
`
)
.catch((err) => {
// Ignore error if type already exists
if (!err.message.includes("already exists")) {
throw err;
}
});
const results = await db
.exec(
`
CREATE TABLE IF NOT EXISTS "public"."TaskRun" (
"id" text NOT NULL,
"idempotencyKey" text,
"payload" text NOT NULL,
"payloadType" text NOT NULL DEFAULT 'application/json'::text,
"context" jsonb,
"runtimeEnvironmentId" text NOT NULL,
"projectId" text NOT NULL,
"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" timestamp NOT NULL,
"taskIdentifier" text NOT NULL,
"lockedAt" timestamp,
"lockedById" text,
"friendlyId" text NOT NULL,
"lockedToVersionId" text,
"traceContext" jsonb,
"spanId" text NOT NULL,
"traceId" text NOT NULL,
"concurrencyKey" text,
"queue" text NOT NULL,
"number" int4 NOT NULL DEFAULT 0,
"isTest" bool NOT NULL DEFAULT false,
"status" "public"."TaskRunStatus" NOT NULL DEFAULT 'PENDING'::"TaskRunStatus",
"scheduleId" text,
"scheduleInstanceId" text,
"startedAt" timestamp,
"usageDurationMs" int4 NOT NULL DEFAULT 0,
"costInCents" float8 NOT NULL DEFAULT 0,
"baseCostInCents" float8 NOT NULL DEFAULT 0,
"machinePreset" text,
"delayUntil" timestamp,
"queuedAt" timestamp,
"expiredAt" timestamp,
"ttl" text,
"maxAttempts" int4,
"completedAt" timestamp,
"logsDeletedAt" timestamp,
"batchId" text,
"depth" int4 NOT NULL DEFAULT 0,
"parentTaskRunAttemptId" text,
"parentTaskRunId" text,
"resumeParentOnCompletion" bool NOT NULL DEFAULT false,
"rootTaskRunId" text,
"parentSpanId" text,
"metadata" text,
"metadataType" text NOT NULL DEFAULT 'application/json'::text,
"output" text,
"outputType" text NOT NULL DEFAULT 'application/json'::text,
"error" jsonb,
"seedMetadata" text,
"seedMetadataType" text NOT NULL DEFAULT 'application/json'::text,
"runTags" _text,
"maxDurationInSeconds" int4,
CONSTRAINT "TaskRun_parentTaskRunId_fkey" FOREIGN KEY ("parentTaskRunId") REFERENCES "public"."TaskRun"("id") ON DELETE SET NULL,
CONSTRAINT "TaskRun_rootTaskRunId_fkey" FOREIGN KEY ("rootTaskRunId") REFERENCES "public"."TaskRun"("id") ON DELETE SET NULL,
PRIMARY KEY ("id")
);
`
)
.catch((error) => {
console.error("Migration failed:", error);
throw error;
});
return db;
}
+63
View File
@@ -0,0 +1,63 @@
import { PGliteProvider } from "@electric-sql/pglite-react";
import { ReactNode, useEffect, useState } from "react";
import { createClient, PGClient } from "./client";
import { useAppOrigin } from "~/root";
export function PgProvider({ projectId, children }: { projectId: string; children: ReactNode }) {
const [db, setDb] = useState<PGClient | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
createClient()
.then((client) => {
setDb(client);
setIsLoading(false);
})
.catch((error) => {
console.error("Failed to initialize database:", error);
setIsLoading(false);
});
}, []);
usePgSync({ pg: db, projectId });
if (isLoading) {
return <div>Loading database...</div>;
}
if (!db) {
return <div>Failed to initialize database</div>;
}
return <PGliteProvider db={db}>{children}</PGliteProvider>;
}
function usePgSync({ pg, projectId }: { pg: PGClient | null; projectId: string }) {
const origin = useAppOrigin();
useEffect(() => {
if (!pg) return;
let shape: { unsubscribe: () => void };
const setupSync = async () => {
try {
shape = await pg.electric.syncShapeToTable({
shape: { url: `${origin}/sync/${projectId}/runs` },
table: "TaskRun",
primaryKey: ["id"],
});
} catch (error) {
console.error("Error syncing shape:", error);
}
};
setupSync();
return () => {
if (shape) {
shape.unsubscribe();
}
};
}, [projectId, pg]);
}
+9
View File
@@ -16,6 +16,7 @@ import { useHighlight } from "./hooks/useHighlight";
import { usePostHog } from "./hooks/usePostHog";
import { getUser } from "./services/session.server";
import { appEnvTitleTag } from "./utils";
import { useTypedMatchData, useTypedMatchesData } from "./hooks/useTypedMatchData";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
@@ -59,6 +60,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
export type LoaderType = typeof loader;
export function useAppOrigin() {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "root",
});
return routeMatch!.appOrigin;
}
export const shouldRevalidate: ShouldRevalidateFunction = (options) => {
if (options.formAction === "/resources/environment") {
return false;
@@ -0,0 +1,424 @@
import { useLiveQuery } from "@electric-sql/pglite-react";
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid";
import { Form, useNavigation } from "@remix-run/react";
import { IconCircleX } from "@tabler/icons-react";
import type { TaskRun } from "@trigger.dev/database";
import { AnimatePresence, motion } from "framer-motion";
import { ListChecks, ListX } from "lucide-react";
import { useState } from "react";
import { TaskIcon } from "~/assets/icons/TaskIcon";
import { StepContentContainer } from "~/components/StepContentContainer";
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "~/components/primitives/Dialog";
import { Header1, Header2 } from "~/components/primitives/Headers";
import { InfoPanel } from "~/components/primitives/InfoPanel";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
SelectedItemsProvider,
useSelectedItems,
} from "~/components/primitives/SelectedItemsProvider";
import { Spinner } from "~/components/primitives/Spinner";
import { StepNumber } from "~/components/primitives/StepNumber";
import { TextLink } from "~/components/primitives/TextLink";
import { useRunFilters } from "~/components/runs/v3/RunFilters";
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
import { BULK_ACTION_RUN_LIMIT } from "~/consts";
import { useOrganization } from "~/hooks/useOrganizations";
import { MatchedProject, useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
import { PgProvider } from "~/pglite/provider";
import { cn } from "~/utils/cn";
import { docsPath, v3ProjectPath, v3RunsPath, v3TestPath } from "~/utils/pathBuilder";
import { isCancellableRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
export default function Page() {
const project = useProject();
return (
<PgProvider projectId={project.id}>
<Content />
</PgProvider>
);
}
function transformRuns({
userId,
project,
runs,
}: {
userId: string;
project: MatchedProject;
runs: TaskRun[];
}) {
return runs.flatMap((run) => {
const hasFinished = isFinalRunStatus(run.status);
const environment = project.environments.find((env) => env.id === run.runtimeEnvironmentId);
if (!environment) return [];
return [
{
...run,
idempotencyKey: run.idempotencyKey ?? undefined,
hasFinished,
createdAt: run.createdAt.toISOString(),
updatedAt: run.updatedAt.toISOString(),
startedAt: run.startedAt?.toISOString(),
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
delayUntil: run.delayUntil?.toISOString(),
expiredAt: run.expiredAt?.toISOString(),
isReplayable: true,
isCancellable: isCancellableRunStatus(run.status),
environment: {
id: environment.id,
type: environment.type,
slug: environment.slug,
userName: userId === environment.userId ? undefined : environment.userName,
},
version: "NEED TO FILL",
tags: run.runTags ? run.runTags : [],
ttl: run.ttl ?? undefined,
},
];
});
}
function Content() {
const user = useUser();
const project = useProject();
const { filters, hasFilters } = useRunFilters();
const items = useLiveQuery<TaskRun>(`SELECT * FROM "TaskRun";`);
console.log(items);
const runs = items
? transformRuns({
userId: user.id,
project,
runs: items.rows,
})
: undefined;
return (
<>
<NavBar>
<PageTitle title="Runs" />
<PageAccessories>
<LinkButton
variant={"docs/small"}
LeadingIcon={BookOpenIcon}
to={docsPath("/runs-and-attempts")}
>
Runs docs
</LinkButton>
</PageAccessories>
</NavBar>
<PageBody scrollable={false}>
<SelectedItemsProvider
initialSelectedItems={[]}
maxSelectedItemCount={BULK_ACTION_RUN_LIMIT}
>
{({ selectedItems }) => (
<div
className={cn(
"grid h-full max-h-full overflow-hidden",
selectedItems.size === 0 ? "grid-rows-1" : "grid-rows-[1fr_auto]"
)}
>
{!runs ? (
<div className="flex items-center justify-center py-2">
<div className="mx-auto flex items-center gap-2">
<Spinner />
<Paragraph variant="small">Loading runs</Paragraph>
</div>
</div>
) : runs.length === 0 && !hasFilters ? (
<CreateFirstTaskInstructions />
) : (
<div className={cn("grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden")}>
<div className="flex items-start justify-between gap-x-2 p-2">
{/* <RunsFilters
possibleEnvironments={project.environments}
possibleTasks={list.possibleTasks}
bulkActions={list.bulkActions}
hasFilters={list.hasFilters}
/> */}
<div className="flex items-center justify-end gap-x-2">
{/* <ListPagination list={list} /> */}
</div>
</div>
<TaskRunsTable
total={runs.length}
hasFilters={true}
filters={filters}
runs={runs}
isLoading={false}
allowSelection
/>
</div>
)}
<BulkActionBar />
</div>
)}
</SelectedItemsProvider>
</PageBody>
</>
);
}
function BulkActionBar() {
const { selectedItems, deselectAll } = useSelectedItems();
const [barState, setBarState] = useState<"none" | "replay" | "cancel">("none");
const hasSelectedMaximum = selectedItems.size >= BULK_ACTION_RUN_LIMIT;
return (
<AnimatePresence>
{selectedItems.size > 0 && (
<motion.div
initial={{ translateY: "100%" }}
animate={{ translateY: 0 }}
exit={{ translateY: "100%" }}
className="flex items-center justify-between gap-3 border-t border-grid-bright bg-background-bright py-3 pl-4 pr-3"
>
<div className="flex items-center gap-1.5 text-sm text-text-bright">
<ListChecks className="mr-1 size-7 text-indigo-400" />
<Header2>Bulk actions:</Header2>
{hasSelectedMaximum ? (
<Paragraph className="text-warning">
Maximum of {selectedItems.size} runs selected
</Paragraph>
) : (
<Paragraph className="">{selectedItems.size} runs selected</Paragraph>
)}
</div>
<div className="flex items-center gap-3">
<CancelRuns
onOpen={(o) => {
if (o) {
setBarState("cancel");
} else {
setBarState("none");
}
}}
/>
<ReplayRuns
onOpen={(o) => {
if (o) {
setBarState("replay");
} else {
setBarState("none");
}
}}
/>
<Button
variant="tertiary/medium"
shortcut={{ key: "esc", enabledOnInputElements: true }}
onClick={() => {
if (barState !== "none") return;
deselectAll();
}}
LeadingIcon={ListX}
leadingIconClassName="text-indigo-400 w-6 h-6"
>
Clear selection
</Button>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
function CancelRuns({ onOpen }: { onOpen: (open: boolean) => void }) {
const { selectedItems } = useSelectedItems();
const organization = useOrganization();
const project = useProject();
const failedRedirect = v3RunsPath(organization, project);
const formAction = `/resources/taskruns/bulk/cancel`;
const navigation = useNavigation();
const isLoading = navigation.formAction === formAction;
return (
<Dialog onOpenChange={(o) => onOpen(o)}>
<DialogTrigger asChild>
<Button
type="button"
variant="tertiary/medium"
shortcut={{ key: "c", enabledOnInputElements: true }}
LeadingIcon={IconCircleX}
leadingIconClassName="text-error w-[1.3rem] h-[1.3rem]"
>
Cancel runs
</Button>
</DialogTrigger>
<DialogContent key="replay">
<DialogHeader>Cancel {selectedItems.size} runs?</DialogHeader>
<DialogDescription className="pt-2">
<Paragraph>
Canceling these runs will stop them from running. Only runs that are not already
finished will be canceled, the others will remain in their existing state.
</Paragraph>
</DialogDescription>
<DialogFooter>
<Form action={formAction} method="post" reloadDocument>
<input type="hidden" name="failedRedirect" value={failedRedirect} />
<input type="hidden" name="organizationSlug" value={organization.slug} />
<input type="hidden" name="projectSlug" value={project.slug} />
{[...selectedItems].map((runId) => (
<input key={runId} type="hidden" name="runIds" value={runId} />
))}
<Button
type="submit"
variant="danger/medium"
LeadingIcon={isLoading ? "spinner-white" : StopCircleIcon}
disabled={isLoading}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{isLoading ? "Canceling..." : `Cancel ${selectedItems.size} runs`}
</Button>
</Form>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function ReplayRuns({ onOpen }: { onOpen: (open: boolean) => void }) {
const { selectedItems } = useSelectedItems();
const organization = useOrganization();
const project = useProject();
const failedRedirect = v3RunsPath(organization, project);
const formAction = `/resources/taskruns/bulk/replay`;
const navigation = useNavigation();
const isLoading = navigation.formAction === formAction;
return (
<Dialog onOpenChange={(o) => onOpen(o)}>
<DialogTrigger asChild>
<Button
type="button"
variant="tertiary/medium"
shortcut={{ key: "r", enabledOnInputElements: true }}
LeadingIcon={ArrowPathIcon}
leadingIconClassName="text-blue-400 w-[1.3rem] h-[1.3rem]"
>
<span className="text-text-bright">Replay {selectedItems.size} runs</span>
</Button>
</DialogTrigger>
<DialogContent key="replay">
<DialogHeader>Replay runs?</DialogHeader>
<DialogDescription className="pt-2">
<Paragraph>
Replaying these runs will create a new run for each with the same payload and
environment as the original. It will use the latest version of the code for each task.
</Paragraph>
</DialogDescription>
<DialogFooter>
<Form action={formAction} method="post" reloadDocument>
<input type="hidden" name="failedRedirect" value={failedRedirect} />
<input type="hidden" name="organizationSlug" value={organization.slug} />
<input type="hidden" name="projectSlug" value={project.slug} />
{[...selectedItems].map((runId) => (
<input key={runId} type="hidden" name="runIds" value={runId} />
))}
<Button
type="submit"
variant="primary/medium"
LeadingIcon={isLoading ? "spinner-white" : ArrowPathIcon}
disabled={isLoading}
shortcut={{ modifiers: ["meta"], key: "enter" }}
>
{isLoading ? "Replaying..." : `Replay ${selectedItems.size} runs`}
</Button>
</Form>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function CreateFirstTaskInstructions() {
const organization = useOrganization();
const project = useProject();
return (
<MainCenteredContainer className="max-w-md">
<InfoPanel
icon={TaskIcon}
iconClassName="text-blue-500"
panelClassName="max-full"
title="Create your first task"
to={v3ProjectPath(organization, project)}
buttonLabel="Create a task"
>
<Paragraph variant="small">
Before running a task, you must first create one. Follow the instructions on the{" "}
<TextLink to={v3ProjectPath(organization, project)}>Tasks</TextLink> page to create a
task, then return here to run it.
</Paragraph>
</InfoPanel>
</MainCenteredContainer>
);
}
function RunTaskInstructions() {
const organization = useOrganization();
const project = useProject();
return (
<MainCenteredContainer className="max-w-prose">
<Header1 className="mb-6 border-b py-2">How to run your tasks</Header1>
<StepNumber stepNumber="A" title="Trigger a test run" />
<StepContentContainer>
<Paragraph spacing>
You can perform a Run with any payload you want, or use one of our examples on the test
page.
</Paragraph>
<LinkButton
to={v3TestPath(organization, project)}
variant="primary/medium"
LeadingIcon={BeakerIcon}
className="inline-flex"
>
Test
</LinkButton>
<div className="mt-6 flex items-center gap-2">
<hr className="w-full" />
<Paragraph variant="extra-extra-small/dimmed/caps">OR</Paragraph>
<hr className="w-full" />
</div>
</StepContentContainer>
<StepNumber stepNumber="B" title="Trigger your task for real" />
<StepContentContainer>
<Paragraph spacing>
Performing a real run depends on the type of Trigger your Task is using.
</Paragraph>
<LinkButton
to="https://trigger.dev/docs"
variant="primary/medium"
LeadingIcon={BookOpenIcon}
className="inline-flex"
>
How to run a task
</LinkButton>
</StepContentContainer>
</MainCenteredContainer>
);
}
@@ -0,0 +1,10 @@
import { Outlet } from "@remix-run/react";
import { PageContainer } from "~/components/layout/AppLayout";
export default function Page() {
return (
<PageContainer>
<Outlet />
</PageContainer>
);
}
@@ -0,0 +1,78 @@
import type { LoaderFunctionArgs } from "@remix-run/node";
import { z } from "zod";
import { $replica } from "~/db.server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
import { getUserId } from "~/services/session.server";
import { longPollingFetch } from "~/utils/longPollingFetch";
const Params = z.object({
projectId: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
try {
const userId = await getUserId(request);
const { projectId } = Params.parse(params);
logger.log(`/sync/${projectId}/runs`, { userId, projectId });
if (!userId) {
return new Response("No user found in cookie", { status: 401 });
}
const project = await $replica.project.findFirst({
select: {
organization: {
select: {
members: {
select: {
userId: true,
},
},
},
},
},
where: {
id: projectId,
},
});
if (!project) {
return new Response("No project found", { status: 404 });
}
const isMember = project.organization.members.some((member) => member.userId === userId);
if (!isMember) {
return new Response("Not a member of this org", { status: 401 });
}
const url = new URL(request.url);
const originUrl = new URL(`${env.ELECTRIC_ORIGIN}/v1/shape/public."TaskRun"`);
url.searchParams.forEach((value, key) => {
originUrl.searchParams.set(key, value);
});
originUrl.searchParams.set("where", `"projectId"='${projectId}'`);
const finalUrl = originUrl.toString();
logger.log("Fetching trace runs data", { url: finalUrl });
return longPollingFetch(finalUrl);
} catch (error) {
if (error instanceof Response) {
// Error responses from longPollingFetch
return error;
} else if (error instanceof TypeError) {
// Unexpected errors
logger.error("Unexpected error in loader:", { error: error.message });
return new Response("An unexpected error occurred", { status: 500 });
} else {
// Unknown errors
logger.error("Unknown error occurred in loader, not Error", { error: JSON.stringify(error) });
return new Response("An unknown error occurred", { status: 500 });
}
}
}
+3
View File
@@ -46,6 +46,9 @@
"@conform-to/react": "^0.6.1",
"@conform-to/zod": "^0.6.1",
"@depot/sdk-node": "^1.0.0",
"@electric-sql/pglite": "^0.2.13",
"@electric-sql/pglite-react": "^0.2.13",
"@electric-sql/pglite-sync": "^0.2.14",
"@electric-sql/react": "^0.3.5",
"@headlessui/react": "^1.7.8",
"@heroicons/react": "^2.0.12",
Binary file not shown.
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
import { worker } from "@electric-sql/pglite/worker";
import { client } from "./client";
worker({
async init(options) {
return client(options);
},
});
+5 -2
View File
@@ -23,9 +23,12 @@ module.exports = {
"superjson",
"prismjs/components/prism-json",
"prismjs/components/prism-typescript",
/^@electric-sql.*/,
],
browserNodeBuiltinsPolyfill: { modules: { path: true, os: true, crypto: true } },
browserNodeBuiltinsPolyfill: {
modules: { path: true, os: true, crypto: true, fs: true, buffer: true },
},
watchPaths: async () => {
return ["../../packages/core/src/**/*", "../../packages/emails/src/**/*"];
return ["../../packages/core/src/**/*", "../../packages/emails/src/**/*", "./pglite/**/*"];
},
};
+14 -1
View File
@@ -26,7 +26,20 @@ app.use("/build", express.static("public/build", { immutable: true, maxAge: "1y"
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static("public", { maxAge: "1h" }));
app.use(
express.static("public", {
setHeaders: (res, path) => {
if (path.endsWith(".wasm")) {
res.set("Content-Type", "application/wasm");
}
if (path.endsWith(".data")) {
// .data files should be served as binary data
res.set("Content-Type", "application/octet-stream");
}
},
maxAge: "1h",
})
);
app.use(morgan("tiny"));
+32
View File
@@ -231,6 +231,15 @@ importers:
'@depot/sdk-node':
specifier: ^1.0.0
version: 1.0.0
'@electric-sql/pglite':
specifier: ^0.2.13
version: 0.2.13
'@electric-sql/pglite-react':
specifier: ^0.2.13
version: 0.2.13(@electric-sql/pglite@0.2.13)(react@18.2.0)
'@electric-sql/pglite-sync':
specifier: ^0.2.14
version: 0.2.14(@electric-sql/pglite@0.2.13)
'@electric-sql/react':
specifier: ^0.3.5
version: 0.3.5(react@18.2.0)
@@ -4809,6 +4818,29 @@ packages:
'@rollup/rollup-darwin-arm64': 4.21.3
dev: false
/@electric-sql/pglite-react@0.2.13(@electric-sql/pglite@0.2.13)(react@18.2.0):
resolution: {integrity: sha512-JNhC16yFOzEl8u1pZuhS5Av2Ly/ZsLdc3qHEd+H5vJilchBIg1p++pT8LLCg6SkCUP85yGq8VMwg2vM4nADTvA==}
peerDependencies:
'@electric-sql/pglite': ^0.2.13
react: ^18.0.0
dependencies:
'@electric-sql/pglite': 0.2.13
react: 18.2.0
dev: false
/@electric-sql/pglite-sync@0.2.14(@electric-sql/pglite@0.2.13):
resolution: {integrity: sha512-k5bqr42zpY80QTbFA/6nHyO4habS0xrIrMRpbTZn+iR1R+Rox5ulrXTpKfYH7uMaTf1A/wPcQHFskzVPrX41LA==}
peerDependencies:
'@electric-sql/pglite': ^0.2.13
dependencies:
'@electric-sql/client': 0.6.3
'@electric-sql/pglite': 0.2.13
dev: false
/@electric-sql/pglite@0.2.13:
resolution: {integrity: sha512-YRY806NnScVqa21/1L1vaysSQ+0/cAva50z7vlwzaGiBOTS9JhdzIRHN0KfgMhobFAphbznZJ7urMso4RtMBIQ==}
dev: false
/@electric-sql/react@0.3.5(react@18.2.0):
resolution: {integrity: sha512-qPrlF3BsRg5L8zAn1sLGzc3pkswfEHyQI3lNOu7Xllv1DBx85RvHR1zgGGPAUfC8iwyWupQu9pFPE63GdbeuhA==}
peerDependencies: