fix(ts-sdk): take /v1/ping/ off the request critical path (#6788)
This commit is contained in:
+67
-35
@@ -83,6 +83,16 @@ interface ClientOptions {
|
|||||||
host?: string;
|
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<string, Promise<ClientIdentity>>();
|
||||||
|
|
||||||
export default class MemoryClient {
|
export default class MemoryClient {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
host: string;
|
host: string;
|
||||||
@@ -91,6 +101,7 @@ export default class MemoryClient {
|
|||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
client: any;
|
client: any;
|
||||||
telemetryId: string;
|
telemetryId: string;
|
||||||
|
private initialized: Promise<void>;
|
||||||
|
|
||||||
_validateApiKey(): any {
|
_validateApiKey(): any {
|
||||||
if (!this.apiKey) {
|
if (!this.apiKey) {
|
||||||
@@ -122,11 +133,39 @@ export default class MemoryClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._validateApiKey();
|
this._validateApiKey();
|
||||||
|
|
||||||
this.telemetryId = "";
|
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<void> {
|
||||||
|
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<ClientIdentity> {
|
||||||
try {
|
try {
|
||||||
await this.ping();
|
await this.ping();
|
||||||
|
|
||||||
@@ -148,6 +187,12 @@ export default class MemoryClient {
|
|||||||
stack: error?.stack || "No stack trace",
|
stack: error?.stack || "No stack trace",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
telemetryId: this.telemetryId,
|
||||||
|
organizationId: this.organizationId,
|
||||||
|
projectId: this.projectId,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _maybeAliasAnonToEmail(): Promise<void> {
|
private async _maybeAliasAnonToEmail(): Promise<void> {
|
||||||
@@ -175,13 +220,18 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _captureEvent(methodName: string, args: any[]) {
|
private _captureEvent(methodName: string, args: any[]) {
|
||||||
captureClientEvent(methodName, this, {
|
// Deferred until ping() has resolved telemetryId, off the request path.
|
||||||
success: true,
|
this.initialized
|
||||||
args_count: args.length,
|
.then(() =>
|
||||||
keys: args.length > 0 ? args[0] : [],
|
captureClientEvent(methodName, this, {
|
||||||
}).catch((error: any) => {
|
success: true,
|
||||||
console.error("Failed to capture event:", error);
|
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<any> {
|
async _fetchWithErrorHandling(url: string, options: any): Promise<any> {
|
||||||
@@ -262,8 +312,6 @@ export default class MemoryClient {
|
|||||||
throw new Error("Cannot process an empty messages payload.");
|
throw new Error("Cannot process an empty messages payload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
|
|
||||||
const payload = this._preparePayload(messages, options);
|
const payload = this._preparePayload(messages, options);
|
||||||
const payloadKeys = Object.keys(payload);
|
const payloadKeys = Object.keys(payload);
|
||||||
this._captureEvent("add", [payloadKeys]);
|
this._captureEvent("add", [payloadKeys]);
|
||||||
@@ -304,7 +352,6 @@ export default class MemoryClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payload: Record<string, any> = {};
|
const payload: Record<string, any> = {};
|
||||||
if (text !== undefined) payload.text = text;
|
if (text !== undefined) payload.text = text;
|
||||||
if (metadata !== undefined) payload.metadata = metadata;
|
if (metadata !== undefined) payload.metadata = metadata;
|
||||||
@@ -326,7 +373,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(memoryId: string): Promise<Memory> {
|
async get(memoryId: string): Promise<Memory> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("get", []);
|
this._captureEvent("get", []);
|
||||||
return this._fetchWithErrorHandling(
|
return this._fetchWithErrorHandling(
|
||||||
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
|
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||||
@@ -340,7 +386,6 @@ export default class MemoryClient {
|
|||||||
// Reject top-level entity params - must use filters instead
|
// Reject top-level entity params - must use filters instead
|
||||||
rejectTopLevelEntityParams(options as Record<string, any>, "getAll");
|
rejectTopLevelEntityParams(options as Record<string, any>, "getAll");
|
||||||
|
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payloadKeys = Object.keys(options || {});
|
const payloadKeys = Object.keys(options || {});
|
||||||
this._captureEvent("get_all", [payloadKeys]);
|
this._captureEvent("get_all", [payloadKeys]);
|
||||||
const { page, pageSize, filters, ...rest } = options ?? {};
|
const { page, pageSize, filters, ...rest } = options ?? {};
|
||||||
@@ -369,7 +414,6 @@ export default class MemoryClient {
|
|||||||
// Reject top-level entity params - must use filters instead
|
// Reject top-level entity params - must use filters instead
|
||||||
rejectTopLevelEntityParams(options as Record<string, any>, "search");
|
rejectTopLevelEntityParams(options as Record<string, any>, "search");
|
||||||
|
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payloadKeys = Object.keys(options || {});
|
const payloadKeys = Object.keys(options || {});
|
||||||
this._captureEvent("search", [payloadKeys]);
|
this._captureEvent("search", [payloadKeys]);
|
||||||
const { filters, ...rest } = options ?? {};
|
const { filters, ...rest } = options ?? {};
|
||||||
@@ -395,7 +439,6 @@ export default class MemoryClient {
|
|||||||
memoryId: string,
|
memoryId: string,
|
||||||
options: DeleteMemoryOptions = {},
|
options: DeleteMemoryOptions = {},
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("delete", [Object.keys(options || {})]);
|
this._captureEvent("delete", [Object.keys(options || {})]);
|
||||||
const snakeOptions = camelToSnakeKeys(this._prepareParams(options));
|
const snakeOptions = camelToSnakeKeys(this._prepareParams(options));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -412,7 +455,6 @@ export default class MemoryClient {
|
|||||||
async deleteAll(
|
async deleteAll(
|
||||||
options: DeleteAllMemoryOptions = {},
|
options: DeleteAllMemoryOptions = {},
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payloadKeys = Object.keys(options || {});
|
const payloadKeys = Object.keys(options || {});
|
||||||
this._captureEvent("delete_all", [payloadKeys]);
|
this._captureEvent("delete_all", [payloadKeys]);
|
||||||
const snakeOptions = camelToSnakeKeys(this._prepareParams(options));
|
const snakeOptions = camelToSnakeKeys(this._prepareParams(options));
|
||||||
@@ -429,7 +471,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async history(memoryId: string): Promise<Array<MemoryHistory>> {
|
async history(memoryId: string): Promise<Array<MemoryHistory>> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("history", []);
|
this._captureEvent("history", []);
|
||||||
const response = await this._fetchWithErrorHandling(
|
const response = await this._fetchWithErrorHandling(
|
||||||
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/history/`,
|
`${this.host}/v1/memories/${encodePathSegment(memoryId)}/history/`,
|
||||||
@@ -444,7 +485,6 @@ export default class MemoryClient {
|
|||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}): Promise<AllUsers> {
|
}): Promise<AllUsers> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("users", []);
|
this._captureEvent("users", []);
|
||||||
let url = `${this.host}/v1/entities/`;
|
let url = `${this.host}/v1/entities/`;
|
||||||
const params: string[] = [];
|
const params: string[] = [];
|
||||||
@@ -464,7 +504,6 @@ export default class MemoryClient {
|
|||||||
entity_id: number;
|
entity_id: number;
|
||||||
entity_type: string;
|
entity_type: string;
|
||||||
}): Promise<{ message: string }> {
|
}): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("delete_user", []);
|
this._captureEvent("delete_user", []);
|
||||||
if (!data.entity_type) {
|
if (!data.entity_type) {
|
||||||
data.entity_type = "user";
|
data.entity_type = "user";
|
||||||
@@ -487,8 +526,6 @@ export default class MemoryClient {
|
|||||||
runId?: string;
|
runId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
|
|
||||||
let to_delete: Array<{ type: string; name: string }> = [];
|
let to_delete: Array<{ type: string; name: string }> = [];
|
||||||
const { userId, agentId, appId, runId } = params;
|
const { userId, agentId, appId, runId } = params;
|
||||||
|
|
||||||
@@ -514,8 +551,14 @@ export default class MemoryClient {
|
|||||||
|
|
||||||
for (const entity of to_delete) {
|
for (const entity of to_delete) {
|
||||||
try {
|
try {
|
||||||
await this.client.delete(
|
// fetch() reuses the pooled connection; axios here defaulted to
|
||||||
`/v2/entities/${encodePathSegment(entity.type)}/${encodePathSegment(entity.name)}/`,
|
// 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) {
|
} catch (error: any) {
|
||||||
throw new APIError(
|
throw new APIError(
|
||||||
@@ -537,7 +580,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async batchUpdate(memories: Array<MemoryUpdateBody>): Promise<string> {
|
async batchUpdate(memories: Array<MemoryUpdateBody>): Promise<string> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("batch_update", []);
|
this._captureEvent("batch_update", []);
|
||||||
const memoriesBody = memories.map((memory) => ({
|
const memoriesBody = memories.map((memory) => ({
|
||||||
memory_id: memory.memoryId,
|
memory_id: memory.memoryId,
|
||||||
@@ -555,7 +597,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async batchDelete(memories: Array<string>): Promise<string> {
|
async batchDelete(memories: Array<string>): Promise<string> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("batch_delete", []);
|
this._captureEvent("batch_delete", []);
|
||||||
const memoriesBody = memories.map((memory) => ({
|
const memoriesBody = memories.map((memory) => ({
|
||||||
memory_id: memory,
|
memory_id: memory,
|
||||||
@@ -572,7 +613,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProject(options: ProjectOptions): Promise<ProjectResponse> {
|
async getProject(options: ProjectOptions): Promise<ProjectResponse> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payloadKeys = Object.keys(options || {});
|
const payloadKeys = Object.keys(options || {});
|
||||||
this._captureEvent("get_project", [payloadKeys]);
|
this._captureEvent("get_project", [payloadKeys]);
|
||||||
const { fields } = options;
|
const { fields } = options;
|
||||||
@@ -598,7 +638,6 @@ export default class MemoryClient {
|
|||||||
async updateProject(
|
async updateProject(
|
||||||
prompts: PromptUpdatePayload,
|
prompts: PromptUpdatePayload,
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("update_project", []);
|
this._captureEvent("update_project", []);
|
||||||
if (!(this.organizationId && this.projectId)) {
|
if (!(this.organizationId && this.projectId)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -619,7 +658,6 @@ export default class MemoryClient {
|
|||||||
|
|
||||||
// WebHooks
|
// WebHooks
|
||||||
async getWebhooks(data?: { projectId?: string }): Promise<Array<Webhook>> {
|
async getWebhooks(data?: { projectId?: string }): Promise<Array<Webhook>> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("get_webhooks", []);
|
this._captureEvent("get_webhooks", []);
|
||||||
const project_id = data?.projectId || this.projectId;
|
const project_id = data?.projectId || this.projectId;
|
||||||
const response = await this._fetchWithErrorHandling(
|
const response = await this._fetchWithErrorHandling(
|
||||||
@@ -632,7 +670,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createWebhook(webhook: WebhookCreatePayload): Promise<Webhook> {
|
async createWebhook(webhook: WebhookCreatePayload): Promise<Webhook> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("create_webhook", []);
|
this._captureEvent("create_webhook", []);
|
||||||
const body = {
|
const body = {
|
||||||
name: webhook.name,
|
name: webhook.name,
|
||||||
@@ -653,7 +690,6 @@ export default class MemoryClient {
|
|||||||
async updateWebhook(
|
async updateWebhook(
|
||||||
webhook: WebhookUpdatePayload,
|
webhook: WebhookUpdatePayload,
|
||||||
): Promise<{ message: string }> {
|
): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("update_webhook", []);
|
this._captureEvent("update_webhook", []);
|
||||||
const body: Record<string, any> = {};
|
const body: Record<string, any> = {};
|
||||||
if (webhook.name != null) body.name = webhook.name;
|
if (webhook.name != null) body.name = webhook.name;
|
||||||
@@ -673,7 +709,6 @@ export default class MemoryClient {
|
|||||||
async deleteWebhook(data: {
|
async deleteWebhook(data: {
|
||||||
webhookId: string;
|
webhookId: string;
|
||||||
}): Promise<{ message: string }> {
|
}): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("delete_webhook", []);
|
this._captureEvent("delete_webhook", []);
|
||||||
const webhook_id = data.webhookId || data;
|
const webhook_id = data.webhookId || data;
|
||||||
const response = await this._fetchWithErrorHandling(
|
const response = await this._fetchWithErrorHandling(
|
||||||
@@ -687,7 +722,6 @@ export default class MemoryClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async feedback(data: FeedbackPayload): Promise<{ message: string }> {
|
async feedback(data: FeedbackPayload): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
const payloadKeys = Object.keys(data || {});
|
const payloadKeys = Object.keys(data || {});
|
||||||
this._captureEvent("feedback", [payloadKeys]);
|
this._captureEvent("feedback", [payloadKeys]);
|
||||||
const response = await this._fetchWithErrorHandling(
|
const response = await this._fetchWithErrorHandling(
|
||||||
@@ -704,7 +738,6 @@ export default class MemoryClient {
|
|||||||
async createMemoryExport(
|
async createMemoryExport(
|
||||||
data: CreateMemoryExportPayload,
|
data: CreateMemoryExportPayload,
|
||||||
): Promise<{ message: string; id: string }> {
|
): Promise<{ message: string; id: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("create_memory_export", []);
|
this._captureEvent("create_memory_export", []);
|
||||||
|
|
||||||
if (!data.filters || !data.schema) {
|
if (!data.filters || !data.schema) {
|
||||||
@@ -734,7 +767,6 @@ export default class MemoryClient {
|
|||||||
async getMemoryExport(
|
async getMemoryExport(
|
||||||
data: GetMemoryExportPayload,
|
data: GetMemoryExportPayload,
|
||||||
): Promise<{ message: string; id: string }> {
|
): Promise<{ message: string; id: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
|
||||||
this._captureEvent("get_memory_export", []);
|
this._captureEvent("get_memory_export", []);
|
||||||
|
|
||||||
if (!data.memoryExportId && !data.filters) {
|
if (!data.memoryExportId && !data.filters) {
|
||||||
|
|||||||
@@ -98,16 +98,23 @@ describe("MemoryClient - deleteUser() (deprecated)", () => {
|
|||||||
|
|
||||||
describe("MemoryClient - deleteUsers()", () => {
|
describe("MemoryClient - deleteUsers()", () => {
|
||||||
test("URL-encodes entity path segments", async () => {
|
test("URL-encodes entity path segments", async () => {
|
||||||
setupMockFetch();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
|
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 });
|
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" });
|
await client.deleteUsers({ userId: "org/team?active#frag" });
|
||||||
|
|
||||||
expect(deleteMock).toHaveBeenCalledWith(
|
expect(
|
||||||
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
|
findFetchCall(
|
||||||
);
|
mock,
|
||||||
|
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
|
||||||
|
"DELETE",
|
||||||
|
),
|
||||||
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user