From 6052252250df410c0d310ac7666fce509efe5d69 Mon Sep 17 00:00:00 2001 From: pratik <10096516+pratikgajjar@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:37:50 -0700 Subject: [PATCH] fix(ts-sdk): take /v1/ping/ off the request critical path (#6788) --- mem0-ts/src/client/mem0.ts | 102 ++++++++++++------ .../client/tests/memoryClient.users.test.ts | 21 ++-- 2 files changed, 81 insertions(+), 42 deletions(-) diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index 3fd19eb39..a521e2485 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -83,6 +83,16 @@ interface ClientOptions { host?: string; } +interface ClientIdentity { + telemetryId: string; + organizationId: string | number | null; + projectId: string | number | null; +} + +// Shares one ping per (host, api key) across clients; FIFO-capped. +const IDENTITY_CACHE_MAX = 50; +const identityByCredentials = new Map>(); + export default class MemoryClient { apiKey: string; host: string; @@ -91,6 +101,7 @@ export default class MemoryClient { headers: Record; client: any; telemetryId: string; + private initialized: Promise; _validateApiKey(): any { if (!this.apiKey) { @@ -122,11 +133,39 @@ export default class MemoryClient { }); this._validateApiKey(); + this.telemetryId = ""; - this._initializeClient(); + + // Requests never wait on this; only telemetry does. + this.initialized = this._resolveIdentity(); } - private async _initializeClient() { + // One ping per credential pair per process, shared via identityByCredentials. + private _resolveIdentity(): Promise { + const credentials = `${this.host}\u0000${this.apiKey}`; + let shared = identityByCredentials.get(credentials); + if (!shared) { + shared = this._initializeClient(); + if (identityByCredentials.size >= IDENTITY_CACHE_MAX) { + identityByCredentials.delete( + identityByCredentials.keys().next().value!, + ); + } + identityByCredentials.set(credentials, shared); + // A failed ping must not be cached, or the process never recovers. + shared.then((identity) => { + if (!identity.telemetryId) identityByCredentials.delete(credentials); + }); + } + return shared.then((identity) => { + this.telemetryId = identity.telemetryId; + if (identity.organizationId != null) + this.organizationId = identity.organizationId; + if (identity.projectId != null) this.projectId = identity.projectId; + }); + } + + private async _initializeClient(): Promise { try { await this.ping(); @@ -148,6 +187,12 @@ export default class MemoryClient { stack: error?.stack || "No stack trace", }); } + + return { + telemetryId: this.telemetryId, + organizationId: this.organizationId, + projectId: this.projectId, + }; } private async _maybeAliasAnonToEmail(): Promise { @@ -175,13 +220,18 @@ export default class MemoryClient { } private _captureEvent(methodName: string, args: any[]) { - captureClientEvent(methodName, this, { - success: true, - args_count: args.length, - keys: args.length > 0 ? args[0] : [], - }).catch((error: any) => { - console.error("Failed to capture event:", error); - }); + // Deferred until ping() has resolved telemetryId, off the request path. + this.initialized + .then(() => + captureClientEvent(methodName, this, { + success: true, + args_count: args.length, + keys: args.length > 0 ? args[0] : [], + }), + ) + .catch((error: any) => { + console.error("Failed to capture event:", error); + }); } async _fetchWithErrorHandling(url: string, options: any): Promise { @@ -262,8 +312,6 @@ export default class MemoryClient { throw new Error("Cannot process an empty messages payload."); } - if (this.telemetryId === "") await this.ping(); - const payload = this._preparePayload(messages, options); const payloadKeys = Object.keys(payload); this._captureEvent("add", [payloadKeys]); @@ -304,7 +352,6 @@ export default class MemoryClient { ); } - if (this.telemetryId === "") await this.ping(); const payload: Record = {}; if (text !== undefined) payload.text = text; if (metadata !== undefined) payload.metadata = metadata; @@ -326,7 +373,6 @@ export default class MemoryClient { } async get(memoryId: string): Promise { - if (this.telemetryId === "") await this.ping(); this._captureEvent("get", []); return this._fetchWithErrorHandling( `${this.host}/v1/memories/${encodePathSegment(memoryId)}/`, @@ -340,7 +386,6 @@ export default class MemoryClient { // Reject top-level entity params - must use filters instead rejectTopLevelEntityParams(options as Record, "getAll"); - if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("get_all", [payloadKeys]); const { page, pageSize, filters, ...rest } = options ?? {}; @@ -369,7 +414,6 @@ export default class MemoryClient { // Reject top-level entity params - must use filters instead rejectTopLevelEntityParams(options as Record, "search"); - if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("search", [payloadKeys]); const { filters, ...rest } = options ?? {}; @@ -395,7 +439,6 @@ export default class MemoryClient { memoryId: string, options: DeleteMemoryOptions = {}, ): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("delete", [Object.keys(options || {})]); const snakeOptions = camelToSnakeKeys(this._prepareParams(options)); // @ts-ignore @@ -412,7 +455,6 @@ export default class MemoryClient { async deleteAll( options: DeleteAllMemoryOptions = {}, ): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("delete_all", [payloadKeys]); const snakeOptions = camelToSnakeKeys(this._prepareParams(options)); @@ -429,7 +471,6 @@ export default class MemoryClient { } async history(memoryId: string): Promise> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("history", []); const response = await this._fetchWithErrorHandling( `${this.host}/v1/memories/${encodePathSegment(memoryId)}/history/`, @@ -444,7 +485,6 @@ export default class MemoryClient { page?: number; pageSize?: number; }): Promise { - if (this.telemetryId === "") await this.ping(); this._captureEvent("users", []); let url = `${this.host}/v1/entities/`; const params: string[] = []; @@ -464,7 +504,6 @@ export default class MemoryClient { entity_id: number; entity_type: string; }): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("delete_user", []); if (!data.entity_type) { data.entity_type = "user"; @@ -487,8 +526,6 @@ export default class MemoryClient { runId?: string; } = {}, ): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); - let to_delete: Array<{ type: string; name: string }> = []; const { userId, agentId, appId, runId } = params; @@ -514,8 +551,14 @@ export default class MemoryClient { for (const entity of to_delete) { try { - await this.client.delete( - `/v2/entities/${encodePathSegment(entity.type)}/${encodePathSegment(entity.name)}/`, + // fetch() reuses the pooled connection; axios here defaulted to + // keepAlive: false, one handshake per entity. + await this._fetchWithErrorHandling( + `${this.host}/v2/entities/${encodePathSegment(entity.type)}/${encodePathSegment(entity.name)}/`, + { + method: "DELETE", + headers: this.headers, + }, ); } catch (error: any) { throw new APIError( @@ -537,7 +580,6 @@ export default class MemoryClient { } async batchUpdate(memories: Array): Promise { - if (this.telemetryId === "") await this.ping(); this._captureEvent("batch_update", []); const memoriesBody = memories.map((memory) => ({ memory_id: memory.memoryId, @@ -555,7 +597,6 @@ export default class MemoryClient { } async batchDelete(memories: Array): Promise { - if (this.telemetryId === "") await this.ping(); this._captureEvent("batch_delete", []); const memoriesBody = memories.map((memory) => ({ memory_id: memory, @@ -572,7 +613,6 @@ export default class MemoryClient { } async getProject(options: ProjectOptions): Promise { - if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("get_project", [payloadKeys]); const { fields } = options; @@ -598,7 +638,6 @@ export default class MemoryClient { async updateProject( prompts: PromptUpdatePayload, ): Promise> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("update_project", []); if (!(this.organizationId && this.projectId)) { throw new Error( @@ -619,7 +658,6 @@ export default class MemoryClient { // WebHooks async getWebhooks(data?: { projectId?: string }): Promise> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("get_webhooks", []); const project_id = data?.projectId || this.projectId; const response = await this._fetchWithErrorHandling( @@ -632,7 +670,6 @@ export default class MemoryClient { } async createWebhook(webhook: WebhookCreatePayload): Promise { - if (this.telemetryId === "") await this.ping(); this._captureEvent("create_webhook", []); const body = { name: webhook.name, @@ -653,7 +690,6 @@ export default class MemoryClient { async updateWebhook( webhook: WebhookUpdatePayload, ): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("update_webhook", []); const body: Record = {}; if (webhook.name != null) body.name = webhook.name; @@ -673,7 +709,6 @@ export default class MemoryClient { async deleteWebhook(data: { webhookId: string; }): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("delete_webhook", []); const webhook_id = data.webhookId || data; const response = await this._fetchWithErrorHandling( @@ -687,7 +722,6 @@ export default class MemoryClient { } async feedback(data: FeedbackPayload): Promise<{ message: string }> { - if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(data || {}); this._captureEvent("feedback", [payloadKeys]); const response = await this._fetchWithErrorHandling( @@ -704,7 +738,6 @@ export default class MemoryClient { async createMemoryExport( data: CreateMemoryExportPayload, ): Promise<{ message: string; id: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("create_memory_export", []); if (!data.filters || !data.schema) { @@ -734,7 +767,6 @@ export default class MemoryClient { async getMemoryExport( data: GetMemoryExportPayload, ): Promise<{ message: string; id: string }> { - if (this.telemetryId === "") await this.ping(); this._captureEvent("get_memory_export", []); if (!data.memoryExportId && !data.filters) { diff --git a/mem0-ts/src/client/tests/memoryClient.users.test.ts b/mem0-ts/src/client/tests/memoryClient.users.test.ts index 5cdb30294..052325d71 100644 --- a/mem0-ts/src/client/tests/memoryClient.users.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.users.test.ts @@ -98,16 +98,23 @@ describe("MemoryClient - deleteUser() (deprecated)", () => { describe("MemoryClient - deleteUsers()", () => { test("URL-encodes entity path segments", async () => { - setupMockFetch(); + const extra = new Map(); + extra.set("/v2/entities/user/org%2Fteam%3Factive%23frag/", { + status: 200, + body: { message: "Entity deleted successfully!" }, + }); + const mock = setupMockFetch(extra); + const client = new MemoryClient({ apiKey: TEST_API_KEY }); - client.telemetryId = "test@example.com"; - const deleteMock = jest.fn().mockResolvedValue({}); - client.client = { delete: deleteMock }; await client.deleteUsers({ userId: "org/team?active#frag" }); - expect(deleteMock).toHaveBeenCalledWith( - "/v2/entities/user/org%2Fteam%3Factive%23frag/", - ); + expect( + findFetchCall( + mock, + "/v2/entities/user/org%2Fteam%3Factive%23frag/", + "DELETE", + ), + ).toBeDefined(); }); });