This commit is contained in:
Agnik Paul 2026-10-05 11:25:59 -07:00 • committed by GitHub
commit 6e1b5c0788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 113 additions and 4 deletions

View file

@ -0,0 +1,66 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import { SupermemoryClient } from "./index"
const API_URL = "https://api.example.com"
describe("SupermemoryClient.getDocuments", () => {
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
function stubFetch() {
const fetchMock = vi.fn().mockResolvedValue(
Response.json({
documents: [],
pagination: {
currentPage: 1,
limit: 200,
totalItems: 0,
totalPages: 0,
},
}),
)
vi.stubGlobal("fetch", fetchMock)
return fetchMock
}
it("cancels through a caller-provided signal", async () => {
const fetchMock = stubFetch()
const controller = new AbortController()
await new SupermemoryClient("sm_test_key", "user_1", API_URL).getDocuments(
["user_1"],
1,
200,
{ signal: controller.signal },
)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
controller.abort()
expect(init.signal?.aborted).toBe(true)
})
it("keeps the timeout when a caller-provided signal is present", async () => {
const timeoutController = new AbortController()
const timeoutSpy = vi
.spyOn(AbortSignal, "timeout")
.mockReturnValue(timeoutController.signal)
const fetchMock = stubFetch()
await new SupermemoryClient("sm_test_key", "user_1", API_URL).getDocuments(
["user_1"],
1,
200,
{ signal: new AbortController().signal },
)
expect(timeoutSpy).toHaveBeenCalledWith(30_000)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
// Firing only the timeout leg aborts the request: a caller signal adds
// cancellation, it does not remove the 30s bound.
timeoutController.abort()
expect(init.signal?.aborted).toBe(true)
})
})

View file

@ -339,7 +339,14 @@ export class SupermemoryClient {
options?: { signal?: AbortSignal },
): Promise<DocumentsApiResponse> {
try {
const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS)
// Compose rather than choose: a caller-supplied signal must add
// cancellation on top of the timeout, not replace it.
const signal = options?.signal
? AbortSignal.any([
options.signal,
AbortSignal.timeout(FETCH_TIMEOUT_MS),
])
: AbortSignal.timeout(FETCH_TIMEOUT_MS)
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
method: "POST",
headers: {

View file

@ -33,7 +33,11 @@ export async function forgetMemoryRequest(
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(params),
signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS),
// Compose rather than choose: a caller-supplied signal must add cancellation
// on top of the timeout, not replace it, or the request becomes unbounded.
signal: options?.signal
? AbortSignal.any([options.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
: AbortSignal.timeout(FETCH_TIMEOUT_MS),
})
if (!response.ok) {

View file

@ -180,7 +180,7 @@ describe("memoryForget", () => {
expect(init.signal).toBeInstanceOf(AbortSignal)
})
it("uses a caller-provided signal instead of creating a timeout", async () => {
it("cancels through a caller-provided signal", async () => {
const fetchMock = stubFetch()
const controller = new AbortController()
@ -192,7 +192,39 @@ describe("memoryForget", () => {
)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(init.signal).toBe(controller.signal)
// The request signal is a composite, not the caller's own, but aborting
// the caller still aborts the request.
expect(init.signal).not.toBe(controller.signal)
controller.abort()
expect(init.signal?.aborted).toBe(true)
})
it("keeps the timeout when a caller-provided signal is present", async () => {
const timeoutController = new AbortController()
const timeoutSpy = vi
.spyOn(AbortSignal, "timeout")
.mockReturnValue(timeoutController.signal)
const fetchMock = stubFetch()
const controller = new AbortController()
try {
await forgetMemoryRequest(
API_KEY,
{ containerTag: "user_1", id: "mem_1" },
undefined,
{ signal: controller.signal },
)
expect(timeoutSpy).toHaveBeenCalledWith(30_000)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
// Firing only the timeout leg aborts the request: a caller signal adds
// cancellation, it does not remove the 30s bound.
timeoutController.abort()
expect(init.signal?.aborted).toBe(true)
} finally {
timeoutSpy.mockRestore()
}
})
it("throws a descriptive error on non-2xx responses", async () => {