diff --git a/package-lock.json b/package-lock.json index 6756ad2..7c728df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@base44/sdk", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@base44/sdk", - "version": "0.7.1", + "version": "0.7.2", "license": "MIT", "dependencies": { - "axios": "^1.6.2" + "axios": "^1.6.2", + "socket.io-client": "^4.7.5" }, "devDependencies": { "@vitest/coverage-istanbul": "^1.0.0", @@ -1235,6 +1236,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1981,6 +1988,45 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3130,7 +3176,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3601,6 +3646,68 @@ "node": ">= 10" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4261,6 +4368,35 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index b3c929f..054ddad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@base44/sdk", - "version": "0.7.1", + "version": "0.7.2", "description": "JavaScript SDK for Base44 API", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -19,7 +19,8 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "axios": "^1.6.2" + "axios": "^1.6.2", + "socket.io-client": "^4.7.5" }, "devDependencies": { "vitest": "^1.0.0", diff --git a/src/client.ts b/src/client.ts index 944ce97..261367c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5,6 +5,14 @@ import { createAuthModule } from "./modules/auth.js"; import { createSsoModule } from "./modules/sso.js"; import { getAccessToken } from "./utils/auth-utils.js"; import { createFunctionsModule } from "./modules/functions.js"; +import { createAgentsModule } from "./modules/agents.js"; +import { RoomsSocket, RoomsSocketConfig } from "./utils/socket-utils.js"; + +export type CreateClientOptions = { + onError?: (error: Error) => void; +}; + +export type Base44Client = ReturnType; /** * Create a Base44 client instance @@ -23,6 +31,8 @@ export function createClient(config: { serviceToken?: string; requiresAuth?: boolean; functionsVersion?: string; + options?: CreateClientOptions; + onRedirectToLogin?: () => void; }) { const { serverUrl = "https://base44.app", @@ -30,17 +40,33 @@ export function createClient(config: { token, serviceToken, requiresAuth = false, - functionsVersion + options, + functionsVersion, + onRedirectToLogin, } = config; + const socketConfig: RoomsSocketConfig = { + serverUrl, + mountPath: "/ws-user-apps/socket.io/", + transports: ["websocket"], + appId, + token, + }; + + const socket = RoomsSocket({ + config: socketConfig, + }); + const headers = { "X-App-Id": String(appId), - } + }; - const functionHeaders = functionsVersion ? { - ...headers, - "Base44-Functions-Version": functionsVersion - } : headers; + const functionHeaders = functionsVersion + ? { + ...headers, + "Base44-Functions-Version": functionsVersion, + } + : headers; const axiosClient = createAxiosClient({ baseURL: `${serverUrl}/api`, @@ -49,6 +75,8 @@ export function createClient(config: { requiresAuth, appId, serverUrl, + onError: options?.onError, + onRedirectToLogin, }); const functionsAxiosClient = createAxiosClient({ @@ -59,6 +87,8 @@ export function createClient(config: { appId, serverUrl, interceptResponses: false, + onError: options?.onError, + onRedirectToLogin, }); const serviceRoleAxiosClient = createAxiosClient({ @@ -67,6 +97,8 @@ export function createClient(config: { token: serviceToken, serverUrl, appId, + onError: options?.onError, + onRedirectToLogin, }); const serviceRoleFunctionsAxiosClient = createAxiosClient({ @@ -76,13 +108,25 @@ export function createClient(config: { serverUrl, appId, interceptResponses: false, + onRedirectToLogin, }); const userModules = { entities: createEntitiesModule(axiosClient, appId), integrations: createIntegrationsModule(axiosClient, appId), - auth: createAuthModule(axiosClient, functionsAxiosClient, appId), + auth: createAuthModule(axiosClient, functionsAxiosClient, appId, { + onRedirectToLogin, + serverUrl, + }), functions: createFunctionsModule(functionsAxiosClient, appId), + agents: createAgentsModule({ + axios: axiosClient, + socket, + appId, + }), + cleanup: () => { + socket.disconnect(); + }, }; const serviceRoleModules = { @@ -90,6 +134,14 @@ export function createClient(config: { integrations: createIntegrationsModule(serviceRoleAxiosClient, appId), sso: createSsoModule(serviceRoleAxiosClient, appId, token), functions: createFunctionsModule(serviceRoleFunctionsAxiosClient, appId), + agents: createAgentsModule({ + axios: serviceRoleAxiosClient, + socket, + appId, + }), + cleanup: () => { + socket.disconnect(); + }, }; // Always try to get token from localStorage or URL parameters @@ -127,6 +179,9 @@ export function createClient(config: { */ setToken(newToken: string) { userModules.auth.setToken(newToken); + socket.updateConfig({ + token: newToken, + }); }, /** @@ -147,10 +202,12 @@ export function createClient(config: { */ get asServiceRole() { if (!serviceToken) { - throw new Error('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + throw new Error( + "Service token is required to use asServiceRole. Please provide a serviceToken when creating the client." + ); } return serviceRoleModules; - } + }, }; return client; @@ -176,17 +233,29 @@ export function createClientFromRequest(request: Request) { let userToken: string | undefined; if (serviceRoleAuthHeader !== null) { - if (serviceRoleAuthHeader === '' || !serviceRoleAuthHeader.startsWith('Bearer ') || serviceRoleAuthHeader.split(' ').length !== 2) { - throw new Error('Invalid authorization header format. Expected "Bearer "'); + if ( + serviceRoleAuthHeader === "" || + !serviceRoleAuthHeader.startsWith("Bearer ") || + serviceRoleAuthHeader.split(" ").length !== 2 + ) { + throw new Error( + 'Invalid authorization header format. Expected "Bearer "' + ); } - serviceRoleToken = serviceRoleAuthHeader.split(' ')[1]; + serviceRoleToken = serviceRoleAuthHeader.split(" ")[1]; } if (authHeader !== null) { - if (authHeader === '' || !authHeader.startsWith('Bearer ') || authHeader.split(' ').length !== 2) { - throw new Error('Invalid authorization header format. Expected "Bearer "'); + if ( + authHeader === "" || + !authHeader.startsWith("Bearer ") || + authHeader.split(" ").length !== 2 + ) { + throw new Error( + 'Invalid authorization header format. Expected "Bearer "' + ); } - userToken = authHeader.split(' ')[1]; + userToken = authHeader.split(" ")[1]; } return createClient({ @@ -194,6 +263,6 @@ export function createClientFromRequest(request: Request) { appId, token: userToken, serviceToken: serviceRoleToken, - functionsVersion: functionsVersion ?? undefined + functionsVersion: functionsVersion ?? undefined, }); } diff --git a/src/index.ts b/src/index.ts index 2002286..7a4b962 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { createClient, createClientFromRequest } from "./client.js"; +import { createClient, createClientFromRequest, type Base44Client } from "./client.js"; import { Base44Error } from "./utils/axios-client.js"; import { getAccessToken, @@ -16,3 +16,7 @@ export { removeAccessToken, getLoginUrl, }; + +export type { Base44Client }; + +export * from "./types.js"; diff --git a/src/modules/agents.ts b/src/modules/agents.ts new file mode 100644 index 0000000..dedcc6c --- /dev/null +++ b/src/modules/agents.ts @@ -0,0 +1,85 @@ +import { RoomsSocket } from "../utils/socket-utils.js"; +import { AgentConversation, AgentMessage } from "./agents.types.js"; +import { AxiosInstance } from "axios"; +import { ModelFilterParams } from "../types.js"; + +export type AgentsModuleConfig = { + axios: AxiosInstance; + socket: ReturnType; + appId: string; +}; + +export function createAgentsModule({ + axios, + socket, + appId, +}: AgentsModuleConfig) { + const baseURL = `/apps/${appId}/agents`; + + const getConversations = () => { + return axios.get(`${baseURL}/conversations`); + }; + + const getConversation = (conversationId: string) => { + return axios.get( + `${baseURL}/conversations/${conversationId}` + ); + }; + + const listConversations = (filterParams: ModelFilterParams) => { + return axios.get(`${baseURL}/conversations`, { + params: filterParams, + }); + }; + + const createConversation = (conversation: { + agent_name: string; + metadata?: Record; + }) => { + return axios.post( + `${baseURL}/conversations`, + conversation + ); + }; + + const addMessage = async ( + conversation: AgentConversation, + message: AgentMessage + ) => { + const room = `/agent-conversations/${conversation.id}`; + await socket.updateModel( + room, + { + ...conversation, + messages: [...(conversation.messages || []), message], + } + ); + return axios.post( + `${baseURL}/conversations/${conversation.id}/messages`, + message + ); + }; + + const subscribeToConversation = ( + conversationId: string, + onUpdate?: (conversation: AgentConversation) => void + ) => { + const room = `/agent-conversations/${conversationId}`; + return socket.subscribeToRoom(room, { + connect: () => {}, + update_model: ({ data: jsonStr }) => { + const conv = JSON.parse(jsonStr) as AgentConversation; + onUpdate?.(conv); + }, + }); + }; + + return { + getConversations, + getConversation, + listConversations, + createConversation, + addMessage, + subscribeToConversation, + }; +} diff --git a/src/modules/agents.types.ts b/src/modules/agents.types.ts new file mode 100644 index 0000000..a6c1337 --- /dev/null +++ b/src/modules/agents.types.ts @@ -0,0 +1,43 @@ +export type AgentConversation = { + id: string; + app_id: string; + agent_name: string; + created_by_id: string; + messages: AgentMessage[]; + metadata?: Record; +}; + +export type AgentMessage = { + id: string; + role: "user" | "assistant" | "system"; + reasoning?: { + start_date: string; + end_date?: string; + content: string; + }; + content?: string | Record | null; + file_urls?: string[] | null; + tool_calls?: + | { + id: string; + name: string; + arguments_string: string; + status: "running" | "success" | "error" | "stopped"; + results?: string | null; + }[] + | null; + + usage?: { prompt_tokens?: number; completion_tokens?: number } | null; + hidden?: boolean; + custom_context?: + | { message: string; data: Record; type: string }[] + | null; + model?: string | null; + checkpoint_id?: string | null; + metadata?: { + created_date: string; + created_by_email: string; + created_by_full_name: string | null; + }; + additional_message_params?: Record; +}; diff --git a/src/modules/app.types.ts b/src/modules/app.types.ts new file mode 100644 index 0000000..fc2f360 --- /dev/null +++ b/src/modules/app.types.ts @@ -0,0 +1,135 @@ + + +export interface AppMessageContent { + content?: string; + file_urls?: string[]; + custom_context?: unknown; + additional_message_params?: Record; + [key: string]: unknown; +} + +export interface AppConversationMessage extends AppMessageContent { + id?: string | null; + role?: "user" | "assistant" | string; +} + +export interface AppConversationLike { + id?: string | null; + messages?: AppMessageContent[] | null; + model?: string; + functions_fail_silently?: boolean; +} + + +export interface DenoProjectLike { + project_id: string + project_name: string + app_id: string + deployment_name_to_info: Record + +} + +export interface AppLike { + id?: string; + conversation?: AppConversationLike | null; + app_stage?: "pending" | "product_flows" | "ready" | string; + created_date?: string; + updated_date?: string; + created_by?: string; + organization_id?: string; + name?: string; + user_description?: string; + entities?: Record; + additional_user_data_schema?: any; + pages?: { [key: string]: string }; + components: { [key: string]: any }; + layout?: string; + globals_css?: string; + agents?: Record; + logo_url?: string; + slug?: string; + public_settings?: "private_with_login" | "public_with_login" | "public_without_login" | "workspace_with_login" | string; + is_blocked?: boolean; + github_repo_url?: string; + main_page?: string; + installable_integrations?: any; + backend_project?: DenoProjectLike; + last_deployed_at?: string; + is_remixable?: boolean; + remixed_from_app_id?: string; + hide_entity_created_by?: boolean; + platform_version?: number; + enable_username_password?: boolean; + auth_config?: AuthConfigLike; + status?: { + state?: string; + details?: any; + last_updated_date?: string; + }; + custom_instructions?: any; + frozen_files?: string[]; + deep_coding_mode?: boolean; + needs_to_add_diff?: boolean; + installed_integration_context_items?: any[]; + model?: string; + is_starred?: boolean; + agents_enabled?: boolean; + categories?: string[]; + functions?: any; + function_names?: string[]; + user_entity?: UserEntityLike; + app_code_hash?: string; + has_backend_functions_enabled?: boolean; +} + +export interface UserLike { + id?: string | null; +} + +export interface UserEntityLike { + type: string; + name: string; + title?: string; + properties?: { + role?: { + type?: string; + description?: string; + enum?: ("admin" | "user" | string)[]; + }; + email?: { + type?: string; + description?: string; + }; + full_name?: { + type?: string; + description?: string; + }; + }; + required: string[]; +} + + +export interface AuthConfigLike { + enable_username_password?: boolean; + enable_google_login?: boolean; + enable_microsoft_login?: boolean; + enable_facebook_login?: boolean; + sso_provider_name?: string; + enable_sso_login?: boolean; +} + + + + +export type LoginInfoResponse = Pick< + AppLike, + | "id" + | "name" + | "slug" + | "logo_url" + | "user_description" + | "updated_date" + | "created_date" + | "auth_config" + | "platform_version" +>; \ No newline at end of file diff --git a/src/modules/auth.ts b/src/modules/auth.ts index d409c4b..db10ed0 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -10,7 +10,11 @@ import { AxiosInstance } from "axios"; export function createAuthModule( axios: AxiosInstance, functionsAxiosClient: AxiosInstance, - appId: string + appId: string, + options: { + serverUrl: string; + onRedirectToLogin?: () => void; + } ) { return { /** @@ -42,6 +46,10 @@ export function createAuthModule( ); } + if (options.onRedirectToLogin) { + options.onRedirectToLogin(); + return; + } // If nextUrl is not provided, use the current URL const redirectUrl = nextUrl || window.location.href; @@ -91,6 +99,7 @@ export function createAuthModule( setToken(token: string, saveToStorage = true) { if (!token) return; + // handle token change for axios clients axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; functionsAxiosClient.defaults.headers.common[ "Authorization" diff --git a/src/modules/types.ts b/src/modules/types.ts new file mode 100644 index 0000000..8f24e94 --- /dev/null +++ b/src/modules/types.ts @@ -0,0 +1,2 @@ +export * from "./app.types.js"; +export * from "./agents.types.js"; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8580f1e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,12 @@ +export * from "./modules/types.js"; + +export type ModelFilterParams = { + q?: Record; + sort?: string | null; + sort_by?: string | null; + limit?: number | null; + skip?: number | null; + fields?: string[] | null; +}; + + diff --git a/src/utils/axios-client.ts b/src/utils/axios-client.ts index d1f7ff9..f5711d1 100644 --- a/src/utils/axios-client.ts +++ b/src/utils/axios-client.ts @@ -89,6 +89,8 @@ export function createAxiosClient({ appId, serverUrl, interceptResponses = true, + onError, + onRedirectToLogin, }: { baseURL: string; headers?: Record; @@ -97,6 +99,8 @@ export function createAxiosClient({ appId: string; serverUrl: string; interceptResponses?: boolean; + onError?: (error: Error) => void; + onRedirectToLogin?: () => void; }) { const client = axios.create({ baseURL, @@ -157,10 +161,14 @@ export function createAxiosClient({ console.log("Authentication required. Redirecting to login..."); // Use a slight delay to allow the error to propagate first setTimeout(() => { - redirectToLogin(serverUrl, appId); + onRedirectToLogin + ? onRedirectToLogin() + : redirectToLogin(serverUrl, appId); }, 100); } + onError?.(base44Error); + return Promise.reject(base44Error); } ); diff --git a/src/utils/socket-utils.ts b/src/utils/socket-utils.ts new file mode 100644 index 0000000..0fc2f97 --- /dev/null +++ b/src/utils/socket-utils.ts @@ -0,0 +1,172 @@ +import { Socket, io } from "socket.io-client"; +import { getAccessToken } from "./auth-utils.js"; + +export type RoomsSocketConfig = { + serverUrl: string; + mountPath: string; + transports: string[]; + appId: string; + token?: string; +}; + +export type TSocketRoom = string; +export type TJsonStr = string; + +type RoomsSocketEventsMap = { + listen: { + connect: () => Promise | void; + update_model: (msg: { + room: string; + data: TJsonStr; + }) => Promise | void; + error: (error: Error) => Promise | void; + }; + emit: { + join: (room: string) => void; + leave: (room: string) => void; + }; +}; + +type TEvent = keyof RoomsSocketEventsMap["listen"]; + +type THandler = RoomsSocketEventsMap["listen"][E]; + +function initializeSocket( + config: RoomsSocketConfig, + handlers: Partial +) { + const socket = io(config.serverUrl, { + path: config.mountPath, + transports: config.transports, + query: { + app_id: config.appId, + token: config.token ?? getAccessToken(), + }, + }) as Socket; + + socket.on("connect", async () => { + console.log("connect", socket.id); + return handlers.connect?.(); + }); + + socket.on("update_model", async (msg) => { + return handlers.update_model?.(msg); + }); + + socket.on("error", async (error) => { + return handlers.error?.(error); + }); + + socket.on("connect_error", async (error) => { + console.error("connect_error", error); + return handlers.error?.(error); + }); + + return socket; +} + +export type RoomsSocket = ReturnType; + +export function RoomsSocket({ config }: { config: RoomsSocketConfig }) { + let currentConfig = { ...config }; + const roomsToListeners: Record< + TSocketRoom, + Partial[] + > = {}; + + const handlers: RoomsSocketEventsMap["listen"] = { + connect: async () => { + const promises: Promise[] = []; + Object.keys(roomsToListeners).forEach((room) => { + joinRoom(room); + const listeners = getListeners(room); + listeners?.forEach(({ connect }) => { + const promise = async () => connect?.(); + promises.push(promise()); + }); + }); + await Promise.all(promises); + }, + update_model: async (msg) => { + const listeners = getListeners(msg.room); + const promises = listeners.map((listener) => + listener.update_model?.(msg) + ); + await Promise.all(promises); + }, + error: async (error) => { + console.error("error", error); + const promises = Object.values(roomsToListeners) + .flat() + .map((listener) => listener.error?.(error)); + await Promise.all(promises); + }, + }; + + let socket = initializeSocket(config, handlers); + + function cleanup() { + disconnect(); + } + + function disconnect() { + if (socket) { + socket.disconnect(); + } + } + + function updateConfig(config: Partial) { + cleanup(); + currentConfig = { + ...currentConfig, + ...config, + }; + socket = initializeSocket(currentConfig, handlers); + } + + function joinRoom(room: string) { + socket.emit("join", room); + } + + function leaveRoom(room: string) { + socket.emit("leave", room); + } + + async function updateModel(room: string, data: any) { + const dataStr = JSON.stringify(data); + return handlers.update_model?.({ room, data: dataStr }); + } + + function getListeners(room: string) { + return roomsToListeners[room]; + } + + const subscribeToRoom = ( + room: TSocketRoom, + handlers: Partial<{ [k in TEvent]: THandler }> + ) => { + if (!roomsToListeners[room]) { + joinRoom(room); + roomsToListeners[room] = []; + } + + roomsToListeners[room].push(handlers); + + return () => { + roomsToListeners[room] = + roomsToListeners[room]?.filter((listener) => listener !== handlers) ?? + []; + if (roomsToListeners[room].length === 0) { + leaveRoom(room); + } + }; + }; + + return { + socket, + subscribeToRoom, + updateConfig, + updateModel, + disconnect, + }; +}