Files
Dorod-Sky/skyvern-frontend/src/store/useOptimisticallyRequestBrowserSessionId.ts
2025-07-23 15:03:35 -04:00

129 lines
3.4 KiB
TypeScript

import { AxiosInstance } from "axios";
import { create as createStore } from "zustand";
import { User } from "@/api/types";
import { lsKeys } from "@/util/env";
export interface BrowserSessionData {
browser_session_id: string | null;
expires_at: number | null; // seconds since epoch
}
interface RunOpts {
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 * 4;
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 =
createStore<OptimisticBrowserSession>(() => ({
get: (user: User, workflowPermanentId: string) => {
return read(makeKey(user, workflowPermanentId));
},
run: async ({ client, user, workflowPermanentId }: RunOpts) => {
if (workflowPermanentId) {
const userKey = makeKey(user, workflowPermanentId);
const exists = read(userKey);
if (exists) {
return exists;
}
const spareKey = makeKey(user, SPARE);
const spare = read(spareKey);
if (spare) {
del(spareKey);
write(userKey, spare);
create(client).then((newSpare) => write(spareKey, newSpare));
return spare;
}
}
const key = makeKey(user, workflowPermanentId);
const browserSessionData = read(key);
if (browserSessionData) {
return browserSessionData;
}
const knew = await create(client);
write(key, knew);
return knew;
},
}));