feat: add sessions endpoint with session management enhancements (#8596)

* 📝 (monitor.py): Add endpoint to get sessions and handle session_id encoding for API requests
📝 (use-get-messages-mutation.ts): Implement a mutation function to fetch messages with query parameters and handle session_id encoding for API requests
📝 (use-get-messages-polling.ts): Ensure proper encoding of session_id for API requests in polling mutation
📝 (use-get-messages.ts): Handle session_id encoding for API requests in messages query
📝 (new-modal.tsx): Implement functions to handle session deletion and proper encoding of session_id for API requests
📝 (utils.ts): Add functions to encode, decode, validate, format, and prepare session IDs for API requests

* 📝 (constants.ts): Add SESSIONS constant to API URLs for monitoring sessions
🔧 (use-delete-messages.ts): Add queryClient to UseRequestProcessor to invalidate sessions query
✨ (use-get-sessions-from-flow.ts): Introduce useGetSessionsFromFlowQuery to fetch sessions from flow
🔧 (use-rename-session.ts): Change refetchQueries to invalidateQueries for useGetSessionsFromFlowQuery
🔧 (custom-new-modal.tsx): Update import path for IOModal to playground-modal
🔧 (session-selector.tsx): Add setActiveSession function to handle setting active session
🔧 (sidebar-open-view.tsx): Add setActiveSession function to handle setting active session
♻️ (new-modal.tsx): Refactor IOModal into playground-modal and update functionality
♻️ (playground-modal.tsx): Refactor IOModal to handle playground-specific functionality
⬆️ (flowStore.ts): Add newChatOnPlayground state and setNewChatOnPlayground function
⬆️ (index.ts): Update FlowStoreType to include newChatOnPlayground and setNewChatOnPlayground

* 🔧 (pyproject.toml): update testpaths to point to the correct directory for tests
✨ (test_session_endpoint.py): add unit tests for sessions endpoint with flow_id filtering
♻️ (session-selector.tsx): refactor to trim editedSession before setting it
♻️ (sidebar-open-view.tsx): refactor to set visibleSession instead of activeSession

* ✨ (use-get-sessions-from-flow.ts): Always include the flow ID as the default session if it's not already present
♻️ (playground-modal.tsx): Refactor setting sessions to include currentFlowId as the default session if not present, and handle visibility of sessions more efficiently

* ♻️ (use-get-messages-mutation.ts): remove unused imports and refactor code for better readability and maintainability

* ✨ (test_session_endpoint.py): refactor test function names for better clarity and consistency

* ✨ (create-new-session-name.ts): add function to generate a new session name based on the current date and time
🔧 (playground-modal.tsx): import createNewSessionName function to dynamically set a new session name when no session is visible

* [autofix.ci] apply automated fixes

* ✨ (monitor.py): rename get_sessions endpoint to get_message_sessions for clarity and consistency
🔧 (constants.ts): remove unused SESSIONS constant from API URLs
🔧 (use-delete-messages.ts): remove commented out code and unnecessary comments
✨ (use-delete-sessions.ts): add functionality to delete sessions in frontend
🔧 (use-get-sessions-from-flow.ts): update API endpoint for getting sessions to match backend changes
🔧 (playground-modal.tsx): add functionality to delete sessions and associated messages in the UI, update UI optimistically, and handle errors appropriately

* [autofix.ci] apply automated fixes

* 🐛 (monitor.py): Fix type hinting issue in delete_messages function
📝 (monitor.py): Add comments and improve readability in test_messages_endpoints.py
📝 (session_endpoint.py): Update endpoint paths for consistency and clarity in test_session_endpoint.py

* [autofix.ci] apply automated fixes

* fix: update SQL statement to use col() for session_id filtering in get_message_sessions function

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-06-23 11:11:07 -03:00 • committed by GitHub
commit 043ba55718
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 672 additions and 65 deletions

View file

@ -12,7 +12,7 @@ export const useDeleteMessages: useMutationFunctionType<
undefined,
DeleteMessagesParams
> = (options?) => {
const { mutate } = UseRequestProcessor();
const { mutate, queryClient } = UseRequestProcessor();
const deleteMessage = async ({ ids }: DeleteMessagesParams): Promise<any> => {
const response = await api.delete(`${getURL("MESSAGES")}`, {
@ -26,7 +26,15 @@ export const useDeleteMessages: useMutationFunctionType<
DeleteMessagesParams,
any,
DeleteMessagesParams
> = mutate(["useDeleteMessages"], deleteMessage, options);
> = mutate(["useDeleteMessages"], deleteMessage, {
...options,
onSettled: (data, error, variables, context) => {
queryClient.invalidateQueries({
queryKey: ["useGetSessionsFromFlowQuery"],
});
options?.onSettled?.(data, error, variables, context);
},
});
return mutation;
};

View file

@ -0,0 +1,41 @@
import { useMutationFunctionType } from "@/types/api";
import { UseMutationResult } from "@tanstack/react-query";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
interface DeleteSessionParams {
sessionId: string;
}
export const useDeleteSession: useMutationFunctionType<
undefined,
DeleteSessionParams
> = (options?) => {
const { mutate, queryClient } = UseRequestProcessor();
const deleteSession = async ({
sessionId,
}: DeleteSessionParams): Promise<any> => {
const response = await api.delete(
`${getURL("MESSAGES")}/session/${sessionId}`,
);
return response.data;
};
const mutation: UseMutationResult<
DeleteSessionParams,
any,
DeleteSessionParams
> = mutate(["useDeleteSession"], deleteSession, {
...options,
onSettled: (data, error, variables, context) => {
queryClient.invalidateQueries({
queryKey: ["useGetSessionsFromFlowQuery"],
});
options?.onSettled?.(data, error, variables, context);
},
});
return mutation;
};

View file

@ -2,7 +2,10 @@ import { useMessagesStore } from "@/stores/messagesStore";
import { UseMutationResult } from "@tanstack/react-query";
import { ColDef, ColGroupDef } from "ag-grid-community";
import { useEffect, useRef } from "react";
import { extractColumnsFromRows } from "../../../../utils/utils";
import {
extractColumnsFromRows,
prepareSessionIdForAPI,
} from "../../../../utils/utils";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
@ -109,7 +112,14 @@ export const useGetMessagesPollingMutation = (
}
if (params) {
config["params"] = { ...config["params"], ...params };
// Process params to ensure session_id is properly encoded
const processedParams = { ...params } as any;
if (processedParams.session_id) {
processedParams.session_id = prepareSessionIdForAPI(
processedParams.session_id,
);
}
config["params"] = { ...config["params"], ...processedParams };
}
const data = await api.get<any>(`${getURL("MESSAGES")}`, config);

View file

@ -3,7 +3,10 @@ import { useMessagesStore } from "@/stores/messagesStore";
import { keepPreviousData } from "@tanstack/react-query";
import { ColDef, ColGroupDef } from "ag-grid-community";
import { useQueryFunctionType } from "../../../../types/api";
import { extractColumnsFromRows } from "../../../../utils/utils";
import {
extractColumnsFromRows,
prepareSessionIdForAPI,
} from "../../../../utils/utils";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
@ -33,7 +36,14 @@ export const useGetMessagesQuery: useQueryFunctionType<
config["params"] = { flow_id: id };
}
if (params) {
config["params"] = { ...config["params"], ...params };
// Process params to ensure session_id is properly encoded
const processedParams = { ...params } as any;
if (processedParams.session_id) {
processedParams.session_id = prepareSessionIdForAPI(
processedParams.session_id,
);
}
config["params"] = { ...config["params"], ...processedParams };
}
if (!isPlaygroundPage) {
return await api.get<any>(`${getURL("MESSAGES")}`, config);

View file

@ -0,0 +1,66 @@
import useFlowStore from "@/stores/flowStore";
import { keepPreviousData } from "@tanstack/react-query";
import { useQueryFunctionType } from "../../../../types/api";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
interface SessionsQueryParams {
id?: string;
}
interface SessionsResponse {
sessions: string[];
}
export const useGetSessionsFromFlowQuery: useQueryFunctionType<
SessionsQueryParams,
SessionsResponse
> = ({ id }, options) => {
const { query } = UseRequestProcessor();
const getSessionsFn = async (id?: string) => {
const isPlaygroundPage = useFlowStore.getState().playgroundPage;
const config = {};
if (id) {
config["params"] = { flow_id: id };
}
if (!isPlaygroundPage) {
return await api.get<string[]>(`${getURL("MESSAGES")}/sessions`, config);
} else {
// For playground mode, get sessions from sessionStorage
const data = JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]");
// Extract unique session IDs from stored messages
const sessionIdsSet = new Set(
data.map((msg: any) => msg.session_id).filter(Boolean),
);
const sessionIds = Array.from(sessionIdsSet);
// Always include the flow ID as the default session if it's not already present
if (id && !sessionIds.includes(id)) {
sessionIds.unshift(id);
}
return {
data: sessionIds,
};
}
};
const responseFn = async () => {
const response = await getSessionsFn(id);
return { sessions: response.data };
};
const queryResult = query(
["useGetSessionsFromFlowQuery", { id }],
responseFn,
{
placeholderData: keepPreviousData,
...options,
},
);
return queryResult;
};

View file

@ -48,10 +48,9 @@ export const useUpdateSessionName: useMutationFunctionType<
const mutation: UseMutationResult<Message[], any, UpdateSessionParams> =
mutate(["useUpdateSessionName"], updateSessionApi, {
...options,
onSettled: (data, variables, context) => {
// Invalidate and refetch relevant queries
queryClient.refetchQueries({
queryKey: ["useGetMessagesQuery"],
onSettled: () => {
queryClient.invalidateQueries({
queryKey: ["useGetSessionsFromFlowQuery"],
});
},
});

View file

@ -1,4 +1,4 @@
import IOModal from "@/modals/IOModal/new-modal";
import IOModal from "@/modals/IOModal/playground-modal";
import { IOModalPropsType } from "@/types/components";
export function CustomIOModal({

View file

@ -26,6 +26,7 @@ export default function SessionSelector({
selectedView,
setSelectedView,
playgroundPage,
setActiveSession,
}: {
deleteSession: (session: string) => void;
session: string;
@ -36,6 +37,7 @@ export default function SessionSelector({
selectedView?: { type: string; id: string };
setSelectedView: (view: { type: string; id: string } | undefined) => void;
playgroundPage: boolean;
setActiveSession: (session: string) => void;
}) {
const clientId = useUtilityStore((state) => state.clientId);
let realFlowId = useFlowsManagerStore((state) => state.currentFlowId);
@ -46,6 +48,9 @@ export default function SessionSelector({
const [editedSession, setEditedSession] = useState(session);
const { mutate: updateSessionName } = useUpdateSessionName();
const inputRef = useRef<HTMLInputElement>(null);
const setNewChatOnPlayground = useFlowStore(
(state) => state.setNewChatOnPlayground,
);
useEffect(() => {
setEditedSession(session);
@ -68,13 +73,13 @@ export default function SessionSelector({
{
onSuccess: () => {
if (isVisible) {
updateVisibleSession(editedSession);
updateVisibleSession(editedSession.trim());
}
if (
selectedView?.type === "Session" &&
selectedView?.id === session
) {
setSelectedView({ type: "Session", id: editedSession });
setSelectedView({ type: "Session", id: editedSession.trim() });
}
},
},

View file

@ -0,0 +1,11 @@
export const createNewSessionName = () => {
return `Session ${new Date().toLocaleString("en-US", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
hour12: false,
second: "2-digit",
timeZone: "UTC",
})}`;
};

View file

@ -1,5 +1,6 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import useFlowStore from "@/stores/flowStore";
import { useVoiceStore } from "@/stores/voiceStore";
import IconComponent from "../../../components/common/genericIconComponent";
import { SidebarOpenViewProps } from "../types/sidebar-open-view";
@ -13,11 +14,16 @@ export const SidebarOpenView = ({
visibleSession,
selectedViewField,
playgroundPage,
setActiveSession,
}: SidebarOpenViewProps) => {
const setNewSessionCloseVoiceAssistant = useVoiceStore(
(state) => state.setNewSessionCloseVoiceAssistant,
);
const setNewChatOnPlayground = useFlowStore(
(state) => state.setNewChatOnPlayground,
);
return (
<>
<div className="flex flex-col pl-3">
@ -40,6 +46,7 @@ export const SidebarOpenView = ({
setvisibleSession(undefined);
setSelectedViewField(undefined);
setNewSessionCloseVoiceAssistant(true);
setNewChatOnPlayground(true);
}}
>
<IconComponent
@ -78,6 +85,9 @@ export const SidebarOpenView = ({
type: "Session",
});
}}
setActiveSession={(session) => {
setActiveSession(session);
}}
/>
))}
</div>

View file

@ -1,18 +1,18 @@
//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react";
import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons";
import { EventDeliveryType } from "@/constants/enums";
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
import {
useDeleteMessages,
useGetMessagesQuery,
} from "@/controllers/API/queries/messages";
import { useDeleteSession } from "@/controllers/API/queries/messages/use-delete-sessions";
import { useGetSessionsFromFlowQuery } from "@/controllers/API/queries/messages/use-get-sessions-from-flow";
import { ENABLE_PUBLISH } from "@/customization/feature-flags";
import { track } from "@/customization/utils/analytics";
import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab";
import { LangflowButtonRedirectTarget } from "@/customization/utils/urls";
import { useUtilityStore } from "@/stores/utilityStore";
import { swatchColors } from "@/utils/styleUtils";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { v5 as uuidv5 } from "uuid";
import { useShallow } from "zustand/react/shallow";
import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react";
@ -27,6 +27,7 @@ import { IOModalPropsType } from "../../types/components";
import { cn, getNumberFromString } from "../../utils/utils";
import BaseModal from "../baseModal";
import { ChatViewWrapper } from "./components/chat-view-wrapper";
import { createNewSessionName } from "./components/chatView/chatInput/components/voice-assistant/helpers/create-new-session-name";
import { SelectedViewField } from "./components/selected-view-field";
import { SidebarOpenView } from "./components/sidebar-open-view";
@ -46,6 +47,13 @@ export default function IOModal({
const buildFlow = useFlowStore((state) => state.buildFlow);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const isBuilding = useFlowStore((state) => state.isBuilding);
const newChatOnPlayground = useFlowStore(
(state) => state.newChatOnPlayground,
);
const setNewChatOnPlayground = useFlowStore(
(state) => state.setNewChatOnPlayground,
);
const { flowIcon, flowId, flowGradient, flowName } = useFlowStore(
useShallow((state) => ({
flowIcon: state.currentFlow?.icon,
@ -76,12 +84,36 @@ export default function IOModal({
: realFlowId;
const [sidebarOpen, setSidebarOpen] = useState(true);
const { mutate: deleteSessionFunction } = useDeleteMessages();
const { mutate: deleteMessagesFunction } = useDeleteMessages();
const { mutate: deleteSessionFunction } = useDeleteSession();
const [visibleSession, setvisibleSession] = useState<string | undefined>(
currentFlowId,
);
const PlaygroundTitle = playgroundPage && flowName ? flowName : "Playground";
const {
data: sessionsFromDb,
isLoading: sessionsLoading,
refetch: refetchSessions,
} = useGetSessionsFromFlowQuery(
{
id: currentFlowId,
},
{ enabled: open },
);
useEffect(() => {
if (sessionsFromDb && !sessionsLoading) {
const sessions = [...sessionsFromDb.sessions];
// Always include the currentFlowId as the default session if it's not already present
if (!sessions.includes(currentFlowId)) {
sessions.unshift(currentFlowId);
}
setSessions(sessions);
}
}, [sessionsFromDb, sessionsLoading, currentFlowId]);
useEffect(() => {
setIOModalOpen(open);
return () => {
@ -90,25 +122,45 @@ export default function IOModal({
}, [open]);
function handleDeleteSession(session_id: string) {
// Update UI optimistically
if (visibleSession === session_id) {
const remainingSessions = sessions.filter((s) => s !== session_id);
if (remainingSessions.length > 0) {
setvisibleSession(remainingSessions[0]);
} else {
setvisibleSession(currentFlowId);
}
}
// Delete the session (which will delete all associated messages on the backend)
deleteSessionFunction(
{
ids: messages
.filter((msg) => msg.session_id === session_id)
.map((msg) => msg.id),
},
{ sessionId: session_id },
{
onSuccess: () => {
// Remove the session from local state
deleteSession(session_id);
// Remove all messages for this session from local state
const messageIdsToRemove = messages
.filter((msg) => msg.session_id === session_id)
.map((msg) => msg.id);
if (messageIdsToRemove.length > 0) {
removeMessages(messageIdsToRemove);
}
setSuccessData({
title: "Session deleted successfully.",
});
deleteSession(session_id);
if (visibleSession === session_id) {
setvisibleSession(undefined);
}
},
onError: () => {
// Revert optimistic UI update on error
if (visibleSession !== session_id) {
setvisibleSession(session_id);
}
setErrorData({
title: "Error deleting Session.",
title: "Error deleting session.",
});
},
},
@ -132,27 +184,24 @@ export default function IOModal({
>(startView());
const messages = useMessagesStore((state) => state.messages);
const [sessions, setSessions] = useState<string[]>(
Array.from(
new Set(
messages
.filter((message) => message.flow_id === currentFlowId)
.map((message) => message.session_id),
),
),
);
const removeMessages = useMessagesStore((state) => state.removeMessages);
const [sessions, setSessions] = useState<string[]>([]);
const [sessionId, setSessionId] = useState<string>(currentFlowId);
const setCurrentSessionId = useUtilityStore(
(state) => state.setCurrentSessionId,
);
const { isFetched: messagesFetched } = useGetMessagesQuery(
{
mode: "union",
id: currentFlowId,
},
{ enabled: open },
);
const { isFetched: messagesFetched, refetch: refetchMessages } =
useGetMessagesQuery(
{
mode: "union",
id: currentFlowId,
params: {
session_id: visibleSession,
},
},
{ enabled: open },
);
const chatValue = useUtilityStore((state) => state.chatValueStore);
const setChatValue = useUtilityStore((state) => state.setChatValueStore);
@ -185,28 +234,28 @@ export default function IOModal({
);
useEffect(() => {
const sessions = new Set<string>();
messages
.filter((message) => message.flow_id === currentFlowId)
.forEach((row) => {
sessions.add(row.session_id);
});
setSessions((prev) => {
if (prev.length < Array.from(sessions).length) {
// set the new session as visible
setvisibleSession(
Array.from(sessions)[Array.from(sessions).length - 1],
);
}
return Array.from(sessions);
});
if (newChatOnPlayground && !sessionsLoading) {
const handleRefetchAndSetSession = async () => {
try {
const result = await refetchSessions();
if (result.data?.sessions && result.data.sessions.length > 0) {
setvisibleSession(
result.data.sessions[result.data.sessions.length - 1],
);
}
} catch (error) {
console.error("Error refetching sessions:", error);
}
};
handleRefetchAndSetSession();
setNewChatOnPlayground(false);
}
}, [messages]);
useEffect(() => {
if (!visibleSession) {
setSessionId(
`Session ${new Date().toLocaleString("en-US", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, second: "2-digit", timeZone: "UTC" })}`,
);
setSessionId(createNewSessionName());
setCurrentSessionId(currentFlowId);
} else if (visibleSession) {
setSessionId(visibleSession);
@ -271,6 +320,35 @@ export default function IOModal({
: getNumberFromString(flowGradient ?? flowId ?? "")) %
swatchColors.length;
const setActiveSession = (session: string) => {
setvisibleSession((prev) => {
if (prev === session) {
return undefined;
}
return session;
});
};
const [hasInitialized, setHasInitialized] = useState(false);
const prevVisibleSessionRef = useRef<string | undefined>(visibleSession);
useEffect(() => {
if (!hasInitialized) {
setHasInitialized(true);
prevVisibleSessionRef.current = visibleSession;
return;
}
if (
open &&
visibleSession &&
prevVisibleSessionRef.current !== visibleSession
) {
refetchMessages();
}
prevVisibleSessionRef.current = visibleSession;
}, [visibleSession]);
return (
<BaseModal
open={open}
@ -336,7 +414,7 @@ export default function IOModal({
</Button>
</ShadTooltip>
</div>
{sidebarOpen && (
{sidebarOpen && !sessionsLoading && (
<SidebarOpenView
sessions={sessions}
setSelectedViewField={setSelectedViewField}
@ -345,6 +423,7 @@ export default function IOModal({
visibleSession={visibleSession}
selectedViewField={selectedViewField}
playgroundPage={!!playgroundPage}
setActiveSession={setActiveSession}
/>
)}
{sidebarOpen && showPublishOptions && (
@ -383,7 +462,7 @@ export default function IOModal({
</div>
)}
<div className="flex h-full min-w-96 flex-grow bg-background">
{selectedViewField && (
{selectedViewField && !sessionsLoading && (
<SelectedViewField
selectedViewField={selectedViewField}
setSelectedViewField={setSelectedViewField}

View file

@ -8,4 +8,5 @@ export type SidebarOpenViewProps = {
visibleSession: string | undefined;
selectedViewField: { type: string; id: string } | undefined;
playgroundPage: boolean;
setActiveSession: (session: string) => void;
};

View file

@ -1061,6 +1061,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
);
set({ dismissedNodes: newDismissedNodes });
},
setNewChatOnPlayground: (newChat: boolean) => {
set({ newChatOnPlayground: newChat });
},
newChatOnPlayground: false,
}));
export default useFlowStore;

View file

@ -285,4 +285,6 @@ export type FlowStoreType = {
setCurrentBuildingNodeId: (nodeIds: string[] | undefined) => void;
clearEdgesRunningByNodes: () => Promise<void>;
updateToolMode: (nodeId: string, toolMode: boolean) => void;
newChatOnPlayground: boolean;
setNewChatOnPlayground: (newChat: boolean) => void;
};

View file

@ -902,3 +902,93 @@ export function getOS() {
return os;
}
/**
* Encodes a session ID for safe URL transmission
* Handles both UUID format and date-time format session IDs
* @param {string} session_id - The session ID to encode
* @returns {string} The URL-encoded session ID
*/
export function encodeSessionId(session_id: string): string {
if (!session_id) return "";
// Use encodeURIComponent to properly encode spaces, commas, colons, etc.
return encodeURIComponent(session_id);
}
/**
* Decodes a session ID from URL encoding
* @param {string} encoded_session_id - The URL-encoded session ID
* @returns {string} The decoded session ID
*/
export function decodeSessionId(encoded_session_id: string): string {
if (!encoded_session_id) return "";
try {
return decodeURIComponent(encoded_session_id);
} catch (error) {
console.warn("Failed to decode session ID:", encoded_session_id, error);
return encoded_session_id; // Return as-is if decoding fails
}
}
/**
* Validates if a string is a valid UUID format
* @param {string} str - The string to validate
* @returns {boolean} True if the string is a valid UUID format
*/
export function isUUID(str: string): boolean {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
/**
* Validates if a string is a date-time session format
* @param {string} str - The string to validate
* @returns {boolean} True if the string appears to be a date-time session format
*/
export function isDateTimeSession(str: string): boolean {
// Check for patterns like "Session Jun 16, 15:44:08" or similar
const dateTimeSessionRegex =
/^Session\s+\w{3}\s+\d{1,2},\s+\d{2}:\d{2}:\d{2}$/;
return dateTimeSessionRegex.test(str);
}
/**
* Formats and normalizes session IDs for consistent handling
* Handles both UUID format and date-time format session IDs
* @param {string} session_id - The session ID to format
* @returns {string} The formatted session ID
*/
export function sessionIdFormatted(session_id: string): string {
if (!session_id) return "";
// Decode if it appears to be URL encoded
let decodedId = session_id;
if (session_id.includes("%") || session_id.includes("+")) {
decodedId = decodeSessionId(session_id);
}
// If it's a UUID, return as-is (already in good format)
if (isUUID(decodedId)) {
return decodedId;
}
// If it's a date-time session, return as-is
if (isDateTimeSession(decodedId)) {
return decodedId;
}
// For any other format, return as-is but ensure it's properly trimmed
return decodedId.trim();
}
/**
* Safely prepares a session ID for API requests
* This function should be used when adding session_id to API parameters
* @param {string} session_id - The session ID to prepare
* @returns {string} The properly encoded session ID for API use
*/
export function prepareSessionIdForAPI(session_id: string): string {
const formatted = sessionIdFormatted(session_id);
return encodeSessionId(formatted);
}