mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-03 19:20:31 +00:00
Jon/keyed browser session id cache (#2928)
This commit is contained in:
parent
c294f338d0
commit
1f795a7d95
3 changed files with 148 additions and 78 deletions
|
@ -5,7 +5,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { getClient } from "@/api/AxiosClient";
|
import { getClient } from "@/api/AxiosClient";
|
||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation, User } from "@/api/types";
|
||||||
import { Timer } from "@/components/Timer";
|
import { Timer } from "@/components/Timer";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
@ -29,11 +29,15 @@ import {
|
||||||
statusIsFinalized,
|
statusIsFinalized,
|
||||||
statusIsRunningOrQueued,
|
statusIsRunningOrQueued,
|
||||||
} from "@/routes/tasks/types";
|
} from "@/routes/tasks/types";
|
||||||
|
import {
|
||||||
|
useOptimisticallyRequestBrowserSessionId,
|
||||||
|
type OptimisticBrowserSession,
|
||||||
|
} from "@/store/useOptimisticallyRequestBrowserSessionId";
|
||||||
|
import { useUser } from "@/hooks/useUser";
|
||||||
|
|
||||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||||
import { NodeActionMenu } from "../NodeActionMenu";
|
import { NodeActionMenu } from "../NodeActionMenu";
|
||||||
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
import { WorkflowBlockIcon } from "../WorkflowBlockIcon";
|
||||||
import { lsKeys } from "@/util/env";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
blockLabel: string; // today, this + wpid act as the identity of a block
|
blockLabel: string; // today, this + wpid act as the identity of a block
|
||||||
|
@ -69,12 +73,24 @@ const blockTypeToTitle = (type: WorkflowBlockType): string => {
|
||||||
|
|
||||||
const getPayload = (opts: {
|
const getPayload = (opts: {
|
||||||
blockLabel: string;
|
blockLabel: string;
|
||||||
|
optimistic: OptimisticBrowserSession;
|
||||||
parameters: Record<string, unknown>;
|
parameters: Record<string, unknown>;
|
||||||
totpIdentifier: string | null;
|
totpIdentifier: string | null;
|
||||||
totpUrl: string | null;
|
totpUrl: string | null;
|
||||||
|
user: User | null;
|
||||||
workflowPermanentId: string;
|
workflowPermanentId: string;
|
||||||
workflowSettings: WorkflowSettingsState;
|
workflowSettings: WorkflowSettingsState;
|
||||||
}): Payload => {
|
}): Payload | null => {
|
||||||
|
if (!opts.user) {
|
||||||
|
toast({
|
||||||
|
variant: "warning",
|
||||||
|
title: "Error",
|
||||||
|
description: "No user found",
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const webhook_url = opts.workflowSettings.webhookCallbackUrl.trim();
|
const webhook_url = opts.workflowSettings.webhookCallbackUrl.trim();
|
||||||
|
|
||||||
let extraHttpHeaders = null;
|
let extraHttpHeaders = null;
|
||||||
|
@ -92,15 +108,12 @@ const getPayload = (opts: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const stored = localStorage.getItem(lsKeys.optimisticBrowserSession);
|
const browserSessionData = opts.optimistic.get(
|
||||||
let browserSessionId: string | null = null;
|
opts.user,
|
||||||
try {
|
opts.workflowPermanentId,
|
||||||
const parsed = JSON.parse(stored ?? "");
|
);
|
||||||
const { browser_session_id } = parsed;
|
|
||||||
browserSessionId = browser_session_id as string;
|
const browserSessionId = browserSessionData?.browser_session_id;
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!browserSessionId) {
|
if (!browserSessionId) {
|
||||||
toast({
|
toast({
|
||||||
|
@ -108,6 +121,8 @@ const getPayload = (opts: {
|
||||||
title: "Error",
|
title: "Error",
|
||||||
description: "No browser session ID found",
|
description: "No browser session ID found",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
|
@ -166,6 +181,8 @@ function NodeHeader({
|
||||||
const { data: workflowRun } = useWorkflowRunQuery();
|
const { data: workflowRun } = useWorkflowRunQuery();
|
||||||
const workflowRunIsRunningOrQueued =
|
const workflowRunIsRunningOrQueued =
|
||||||
workflowRun && statusIsRunningOrQueued(workflowRun);
|
workflowRun && statusIsRunningOrQueued(workflowRun);
|
||||||
|
const optimistic = useOptimisticallyRequestBrowserSessionId();
|
||||||
|
const user = useUser().get();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
|
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
|
||||||
|
@ -227,13 +244,19 @@ function NodeHeader({
|
||||||
|
|
||||||
const body = getPayload({
|
const body = getPayload({
|
||||||
blockLabel,
|
blockLabel,
|
||||||
|
optimistic,
|
||||||
parameters,
|
parameters,
|
||||||
totpIdentifier,
|
totpIdentifier,
|
||||||
totpUrl,
|
totpUrl,
|
||||||
|
user,
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
workflowSettings: workflowSettingsStore,
|
workflowSettings: workflowSettingsStore,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return await client.post<Payload, { data: { run_id: string } }>(
|
return await client.post<Payload, { data: { run_id: string } }>(
|
||||||
"/run/workflows/blocks",
|
"/run/workflows/blocks",
|
||||||
body,
|
body,
|
||||||
|
@ -268,8 +291,10 @@ function NodeHeader({
|
||||||
const cancelBlock = useMutation({
|
const cancelBlock = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const browserSessionId =
|
const browserSessionId =
|
||||||
debugStore.getCurrentBrowserSessionId() ??
|
user && workflowPermanentId
|
||||||
"<missing-browser-session-id>";
|
? optimistic.get(user, workflowPermanentId)?.browser_session_id ??
|
||||||
|
"<missing-browser-session-id>"
|
||||||
|
: "<missing-user-or-workflow-permanent-id>";
|
||||||
const client = await getClient(credentialGetter);
|
const client = await getClient(credentialGetter);
|
||||||
return client
|
return client
|
||||||
.post(`/runs/${browserSessionId}/workflow_run/${workflowRunId}/cancel/`)
|
.post(`/runs/${browserSessionId}/workflow_run/${workflowRunId}/cancel/`)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { createContext, useMemo } from "react";
|
import React, { createContext, useMemo } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { lsKeys } from "@/util/env";
|
|
||||||
|
|
||||||
function useIsDebugMode() {
|
function useIsDebugMode() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -10,23 +9,8 @@ function useIsDebugMode() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentBrowserSessionId() {
|
|
||||||
const stored = localStorage.getItem(lsKeys.optimisticBrowserSession);
|
|
||||||
let browserSessionId: string | null = null;
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(stored ?? "");
|
|
||||||
const { browser_session_id } = parsed;
|
|
||||||
browserSessionId = browser_session_id as string;
|
|
||||||
} catch {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
return browserSessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DebugStoreContextType = {
|
export type DebugStoreContextType = {
|
||||||
isDebugMode: boolean;
|
isDebugMode: boolean;
|
||||||
getCurrentBrowserSessionId: () => string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DebugStoreContext = createContext<
|
export const DebugStoreContext = createContext<
|
||||||
|
@ -39,9 +23,7 @@ export const DebugStoreProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
const isDebugMode = useIsDebugMode();
|
const isDebugMode = useIsDebugMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DebugStoreContext.Provider
|
<DebugStoreContext.Provider value={{ isDebugMode }}>
|
||||||
value={{ isDebugMode, getCurrentBrowserSessionId }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</DebugStoreContext.Provider>
|
</DebugStoreContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { AxiosInstance } from "axios";
|
import { AxiosInstance } from "axios";
|
||||||
|
import { create as createStore } from "zustand";
|
||||||
|
|
||||||
|
import { User } from "@/api/types";
|
||||||
import { lsKeys } from "@/util/env";
|
import { lsKeys } from "@/util/env";
|
||||||
|
|
||||||
export interface BrowserSessionData {
|
export interface BrowserSessionData {
|
||||||
|
@ -7,59 +9,120 @@ export interface BrowserSessionData {
|
||||||
expires_at: number | null; // seconds since epoch
|
expires_at: number | null; // seconds since epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OptimisticBrowserSessionIdState extends BrowserSessionData {
|
interface RunOpts {
|
||||||
run: (client: AxiosInstance) => Promise<BrowserSessionData>;
|
client: AxiosInstance;
|
||||||
|
reason?: string;
|
||||||
|
user: User;
|
||||||
|
workflowPermanentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptimisticBrowserSession {
|
||||||
|
get: (user: User, workflowPermanentId: string) => BrowserSessionData | null;
|
||||||
|
run: (runOpts: RunOpts) => Promise<BrowserSessionData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_TIMEOUT_MINUTES = 60;
|
const SESSION_TIMEOUT_MINUTES = 60;
|
||||||
|
const SPARE = "spare";
|
||||||
|
|
||||||
|
const makeKey = (user: User, workflowPermanentId?: string | undefined) => {
|
||||||
|
return `${lsKeys.optimisticBrowserSession}:${user.id}:${workflowPermanentId ?? SPARE}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a `BrowserSessionData` from localStorage cache. If the entry is expired,
|
||||||
|
* return `null`. If the entry is invalid, return `null`. Otherwise return it.
|
||||||
|
*/
|
||||||
|
const read = (key: string): BrowserSessionData | null => {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
const { browser_session_id, expires_at } = parsed;
|
||||||
|
const now = Math.floor(Date.now() / 1000); // seconds since epoch
|
||||||
|
|
||||||
|
if (
|
||||||
|
browser_session_id &&
|
||||||
|
typeof browser_session_id === "string" &&
|
||||||
|
expires_at &&
|
||||||
|
typeof expires_at === "number" &&
|
||||||
|
now < expires_at
|
||||||
|
) {
|
||||||
|
return { browser_session_id, expires_at };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a `BrowserSessionData` to localStorage cache.
|
||||||
|
*/
|
||||||
|
const write = (key: string, browserSessionData: BrowserSessionData) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(browserSessionData));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a localStorage key.
|
||||||
|
*/
|
||||||
|
const del = (key: string) => {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new browser session and return the `BrowserSessionData`.
|
||||||
|
*/
|
||||||
|
const create = async (client: AxiosInstance): Promise<BrowserSessionData> => {
|
||||||
|
const resp = await client.post("/browser_sessions", {
|
||||||
|
timeout: SESSION_TIMEOUT_MINUTES,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { browser_session_id: newBrowserSessionId, timeout } = resp.data;
|
||||||
|
const newExpiresAt = Math.floor(Date.now() / 1000) + timeout * 60 * 0.9;
|
||||||
|
|
||||||
|
return {
|
||||||
|
browser_session_id: newBrowserSessionId,
|
||||||
|
expires_at: newExpiresAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const useOptimisticallyRequestBrowserSessionId =
|
export const useOptimisticallyRequestBrowserSessionId =
|
||||||
create<OptimisticBrowserSessionIdState>((set) => ({
|
createStore<OptimisticBrowserSession>(() => ({
|
||||||
browser_session_id: null,
|
get: (user: User, workflowPermanentId: string) => {
|
||||||
expires_at: null,
|
return read(makeKey(user, workflowPermanentId));
|
||||||
run: async (client) => {
|
},
|
||||||
const stored = localStorage.getItem(lsKeys.optimisticBrowserSession);
|
run: async ({ client, user, workflowPermanentId }: RunOpts) => {
|
||||||
if (stored) {
|
if (workflowPermanentId) {
|
||||||
try {
|
const userKey = makeKey(user, workflowPermanentId);
|
||||||
const parsed = JSON.parse(stored);
|
const exists = read(userKey);
|
||||||
const { browser_session_id, expires_at } = parsed;
|
|
||||||
const now = Math.floor(Date.now() / 1000); // seconds since epoch
|
|
||||||
|
|
||||||
if (
|
if (exists) {
|
||||||
browser_session_id &&
|
return exists;
|
||||||
typeof browser_session_id === "string" &&
|
}
|
||||||
expires_at &&
|
|
||||||
typeof expires_at === "number" &&
|
const spareKey = makeKey(user, SPARE);
|
||||||
now < expires_at
|
const spare = read(spareKey);
|
||||||
) {
|
|
||||||
set({ browser_session_id, expires_at });
|
if (spare) {
|
||||||
return { browser_session_id, expires_at };
|
del(spareKey);
|
||||||
}
|
write(userKey, spare);
|
||||||
} catch (e) {
|
create(client).then((newSpare) => write(spareKey, newSpare));
|
||||||
// pass
|
return spare;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await client.post("/browser_sessions", {
|
const key = makeKey(user, workflowPermanentId);
|
||||||
timeout: SESSION_TIMEOUT_MINUTES,
|
const browserSessionData = read(key);
|
||||||
});
|
|
||||||
const { browser_session_id: newBrowserSessionId, timeout } = resp.data;
|
|
||||||
const newExpiresAt = Math.floor(Date.now() / 1000) + timeout * 60 * 0.9;
|
|
||||||
set({
|
|
||||||
browser_session_id: newBrowserSessionId,
|
|
||||||
expires_at: newExpiresAt,
|
|
||||||
});
|
|
||||||
localStorage.setItem(
|
|
||||||
lsKeys.optimisticBrowserSession,
|
|
||||||
JSON.stringify({
|
|
||||||
browser_session_id: newBrowserSessionId,
|
|
||||||
expires_at: newExpiresAt,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
if (browserSessionData) {
|
||||||
browser_session_id: newBrowserSessionId,
|
return browserSessionData;
|
||||||
expires_at: newExpiresAt,
|
}
|
||||||
};
|
|
||||||
|
const knew = await create(client);
|
||||||
|
write(key, knew);
|
||||||
|
|
||||||
|
return knew;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Add table
Reference in a new issue