UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

550 lines (503 loc) • 18.8 kB
import { describe, expect, it, vi } from "vitest"; import { createInMemoryResumableStreamStore } from "./InMemoryResumableStreamStore"; import type { ResumableStreamEntry } from "../types"; const bytes = (s: string): Uint8Array => new TextEncoder().encode(s); const decode = (chunk: Uint8Array): string => new TextDecoder().decode(chunk); async function drain( iter: AsyncIterable<ResumableStreamEntry>, ): Promise<string[]> { const out: string[] = []; for await (const entry of iter) out.push(decode(entry.chunk)); return out; } describe("InMemoryResumableStreamStore", () => { it.each([10, 1000])( "does not sweep %i retained streams before an expiry is due", async (count) => { let time = 1000; const store = createInMemoryResumableStreamStore({ now: () => time, defaultTtlMs: 100, }); for (let i = 0; i < count; i++) await store.acquire(`stream-${i}`); let scannedEntries = 0; const iterate = Map.prototype[Symbol.iterator]; const spy = vi .spyOn(Map.prototype, Symbol.iterator) .mockImplementation(function* (this: Map<unknown, unknown>) { for (const entry of iterate.call(this)) { if (this.has("stream-0")) scannedEntries += 1; yield entry; } return undefined; }); try { time = 1050; await store.append("stream-0", bytes("one")); expect(await store.status("stream-0")).toBe("streaming"); expect(scannedEntries).toBe(0); time = 1100; expect(await store.status("stream-1")).toBe("missing"); expect(scannedEntries).toBe(count); scannedEntries = 0; await store.append("stream-0", bytes("two")); expect(scannedEntries).toBe(0); } finally { spy.mockRestore(); store.dispose(); } }, ); it("honors a shorter per-stream TTL and reclaims capacity at the deadline", async () => { let time = 1000; const store = createInMemoryResumableStreamStore({ now: () => time, defaultTtlMs: 1000, maxStreams: 2, }); await store.acquire("long"); await store.acquire("short", { ttlMs: 10 }); time = 1010; expect(await store.acquire("replacement")).toBe("producer"); expect(await store.status("short")).toBe("missing"); expect(await store.status("long")).toBe("streaming"); }); it("keeps refreshed and finalized streams until their new deadlines", async () => { let time = 1000; const store = createInMemoryResumableStreamStore({ now: () => time, defaultTtlMs: 100, }); await store.acquire("active"); await store.acquire("finished"); time = 1080; await store.append("active", bytes("x")); await store.finalize("finished", "done"); time = 1100; expect(await store.status("active")).toBe("streaming"); expect(await store.status("finished")).toBe("done"); time = 1180; expect(await store.status("active")).toBe("missing"); expect(await store.status("finished")).toBe("missing"); }); it("wakes a waiting reader when its stream expires", async () => { vi.useFakeTimers(); const store = createInMemoryResumableStreamStore({ defaultTtlMs: 100 }); try { await store.acquire("waiting"); const iterator = store .read("waiting", "", new AbortController().signal) [Symbol.asyncIterator](); const pending = expect(iterator.next()).rejects.toThrow("Stream expired"); await vi.advanceTimersByTimeAsync(100); await pending; } finally { store.dispose(); vi.useRealTimers(); } }); it.each(["default", "per-stream"])( "waits for updates without overflowing native timers with a long %s TTL", async (source) => { const ttlMs = 30 * 24 * 60 * 60 * 1000; const store = createInMemoryResumableStreamStore( source === "default" ? { defaultTtlMs: ttlMs } : {}, ); const ac = new AbortController(); const warnings = vi .spyOn(process, "emitWarning") .mockImplementation(() => {}); try { await store.acquire( "long", source === "per-stream" ? { ttlMs } : undefined, ); const iterator = store .read("long", "", ac.signal) [Symbol.asyncIterator](); const pending = iterator.next(); await store.append("long", bytes("update")); expect(await pending).toEqual({ done: false, value: { cursor: "1", chunk: bytes("update") }, }); const waiting = iterator.next(); ac.abort(); expect(await waiting).toEqual({ done: true, value: undefined }); expect(warnings).not.toHaveBeenCalled(); } finally { ac.abort(); store.dispose(); warnings.mockRestore(); } }, ); it("keeps a long TTL past the timer limit and expires at its full deadline", async () => { vi.useFakeTimers(); vi.setSystemTime(0); const ttlMs = 30 * 24 * 60 * 60 * 1000; const store = createInMemoryResumableStreamStore({ defaultTtlMs: ttlMs }); const ac = new AbortController(); const settled = vi.fn(); try { await store.acquire("long"); const iterator = store .read("long", "", ac.signal) [Symbol.asyncIterator](); const pending = iterator.next().then(settled, settled); await vi.advanceTimersToNextTimerAsync(); expect(Date.now()).toBe(2_147_483_647); expect(settled).not.toHaveBeenCalled(); expect(await store.status("long")).toBe("streaming"); await vi.advanceTimersToNextTimerAsync(); await pending; expect(Date.now()).toBe(ttlMs); expect(settled).toHaveBeenCalledExactlyOnceWith( new Error("Stream expired"), ); expect(await store.status("long")).toBe("missing"); expect(vi.getTimerCount()).toBe(0); } finally { ac.abort(); store.dispose(); vi.useRealTimers(); } }); it("keeps replay bytes independent of producer and consumer buffers", async () => { const store = createInMemoryResumableStreamStore(); const chunk = Buffer.from("a"); await store.acquire("a"); await store.append("a", chunk); await store.finalize("a", "done"); chunk[0] = 98; const signal = new AbortController().signal; const seen: string[] = []; for await (const entry of store.read("a", "", signal)) { seen.push(decode(entry.chunk)); entry.chunk[0] = 99; } expect(seen).toEqual(["a"]); expect(await drain(store.read("a", "", signal))).toEqual(["a"]); }); it("elects exactly one producer per stream id", async () => { const store = createInMemoryResumableStreamStore(); const first = await store.acquire("a"); const second = await store.acquire("a"); const third = await store.acquire("a"); expect(first).toBe("producer"); expect(second).toBe("consumer"); expect(third).toBe("consumer"); }); it("isolates streams by id", async () => { const store = createInMemoryResumableStreamStore(); expect(await store.acquire("a")).toBe("producer"); expect(await store.acquire("b")).toBe("producer"); }); it("replays buffered entries and tails new ones until finalize", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.append("a", bytes("hello ")); await store.append("a", bytes("world")); const ac = new AbortController(); const collected: string[] = []; const reading = (async () => { for await (const entry of store.read("a", "", ac.signal)) { collected.push(decode(entry.chunk)); if (collected.length === 3) { await store.finalize("a", "done"); } } })(); await store.append("a", bytes("!")); await reading; expect(collected).toEqual(["hello ", "world", "!"]); }); it("status transitions: missing → streaming → done", async () => { const store = createInMemoryResumableStreamStore(); expect(await store.status("a")).toBe("missing"); await store.acquire("a"); expect(await store.status("a")).toBe("streaming"); await store.finalize("a", "done"); expect(await store.status("a")).toBe("done"); }); it("status reports error after error finalize", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.finalize("a", "error", "boom"); expect(await store.status("a")).toBe("error"); }); it("read throws on the next iteration after error finalize", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.append("a", bytes("partial")); await store.finalize("a", "error", "boom"); const ac = new AbortController(); const seen: string[] = []; await expect(async () => { for await (const entry of store.read("a", "", ac.signal)) { seen.push(decode(entry.chunk)); } }).rejects.toThrow("boom"); expect(seen).toEqual(["partial"]); }); it("a consumer joining after done replays everything", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.append("a", bytes("a")); await store.append("a", bytes("b")); await store.append("a", bytes("c")); await store.finalize("a", "done"); const ac = new AbortController(); expect(await drain(store.read("a", "", ac.signal))).toEqual([ "a", "b", "c", ]); }); it("cursor advances and skips already-seen entries", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.append("a", bytes("1")); await store.append("a", bytes("2")); await store.append("a", bytes("3")); await store.finalize("a", "done"); const ac = new AbortController(); const seen: { cursor: string; text: string }[] = []; for await (const entry of store.read("a", "", ac.signal)) { seen.push({ cursor: entry.cursor, text: decode(entry.chunk) }); } expect(seen.map((s) => s.text)).toEqual(["1", "2", "3"]); const afterFirst = seen[0]!.cursor; expect(await drain(store.read("a", afterFirst, ac.signal))).toEqual([ "2", "3", ]); }); it("honors abort after the last buffered chunk before a stored error", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.append("a", bytes("partial")); await store.finalize("a", "error", "boom"); const ac = new AbortController(); const seen: string[] = []; for await (const entry of store.read("a", "", ac.signal)) { seen.push(decode(entry.chunk)); ac.abort(); } expect(seen).toEqual(["partial"]); }); it("delivers live updates to every reader and isolates cancellation", async ({ onTestFinished, }) => { const store = createInMemoryResumableStreamStore(); const first = new AbortController(); const second = new AbortController(); const third = new AbortController(); onTestFinished(() => { first.abort(); second.abort(); third.abort(); store.dispose(); }); await store.acquire("a"); const a = store.read("a", "", first.signal)[Symbol.asyncIterator](); const b = store.read("a", "", second.signal)[Symbol.asyncIterator](); const c = store.read("a", "", third.signal)[Symbol.asyncIterator](); for (const [cursor, text] of [ ["1", "x"], ["2", "y"], ] as const) { const pending = Promise.all([a.next(), b.next(), c.next()]); await store.append("a", bytes(text)); const entry = { done: false, value: { cursor, chunk: bytes(text) } }; expect(await pending).toEqual([entry, entry, entry]); } const unpaused = Promise.all([b.next(), c.next()]); await store.append("a", bytes("p")); const paused = { done: false, value: { cursor: "3", chunk: bytes("p") } }; expect(await unpaused).toEqual([paused, paused]); expect(await a.next()).toEqual(paused); const cancelled = a.next(); const continuing = Promise.all([b.next(), c.next()]); first.abort(); expect(await cancelled).toEqual({ done: true, value: undefined }); await store.append("a", bytes("z")); const entry = { done: false, value: { cursor: "4", chunk: bytes("z") } }; expect(await continuing).toEqual([entry, entry]); const finished = Promise.all([b.next(), c.next()]); await store.finalize("a", "done"); expect(await finished).toEqual([ { done: true, value: undefined }, { done: true, value: undefined }, ]); }); it("delete prevents further reads and ends in-flight reads", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); const ac = new AbortController(); const reading = drain(store.read("a", "", ac.signal)); await store.append("a", bytes("x")); await new Promise((r) => setTimeout(r, 0)); await store.delete("a"); expect(await reading).toEqual(["x"]); expect(await store.status("a")).toBe("missing"); }); it("expired streams are evicted on next access", async () => { let now = 1_000; const store = createInMemoryResumableStreamStore({ defaultTtlMs: 100, now: () => now, }); await store.acquire("a"); await store.append("a", bytes("hi")); expect(await store.status("a")).toBe("streaming"); now += 200; expect(await store.status("a")).toBe("missing"); }); it("appending refreshes TTL", async () => { let now = 1_000; const store = createInMemoryResumableStreamStore({ defaultTtlMs: 100, now: () => now, }); await store.acquire("a"); now += 80; await store.append("a", bytes("x")); now += 80; expect(await store.status("a")).toBe("streaming"); }); it("rejects append on finalized stream", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.finalize("a", "done"); await expect(store.append("a", bytes("late"))).rejects.toThrow( /already finalized/, ); }); it("rejects append on missing stream", async () => { const store = createInMemoryResumableStreamStore(); await expect(store.append("a", bytes("x"))).rejects.toThrow( /Stream not found/, ); }); it("finalize is idempotent", async () => { const store = createInMemoryResumableStreamStore(); await store.acquire("a"); await store.finalize("a", "done"); await store.finalize("a", "done"); expect(await store.status("a")).toBe("done"); }); it("rejects append when chunk exceeds maxChunkBytes", async () => { const store = createInMemoryResumableStreamStore({ maxChunkBytes: 4 }); await store.acquire("a"); await expect(store.append("a", bytes("hello"))).rejects.toThrow( /Chunk exceeds maxChunkBytes: 5/, ); await store.append("a", bytes("ok")); expect(await store.status("a")).toBe("streaming"); }); it("rejects append when stream reaches maxEntriesPerStream", async () => { const store = createInMemoryResumableStreamStore({ maxEntriesPerStream: 2, }); await store.acquire("a"); await store.append("a", bytes("1")); await store.append("a", bytes("2")); await expect(store.append("a", bytes("3"))).rejects.toThrow( /Stream exceeded maxEntriesPerStream: a/, ); }); it("rejects acquire when active stream count exceeds maxStreams", async () => { const store = createInMemoryResumableStreamStore({ maxStreams: 2 }); await store.acquire("a"); await store.acquire("b"); await expect(store.acquire("c")).rejects.toThrow(/maxStreams exceeded/); expect(await store.acquire("a")).toBe("consumer"); }); it("gc sweeper evicts expired streams without explicit access", async () => { vi.useFakeTimers(); try { let now = 1_000; const store = createInMemoryResumableStreamStore({ defaultTtlMs: 100, gcIntervalMs: 50, now: () => now, }); await store.acquire("a"); await store.append("a", bytes("hi")); now += 200; vi.advanceTimersByTime(50); // Resetting the clock prevents status() from performing the eviction. now = 1_000; expect(await store.status("a")).toBe("missing"); store.dispose(); } finally { vi.useRealTimers(); } }); it("dispose clears the gc interval", async () => { vi.useFakeTimers(); try { const clearSpy = vi.spyOn(globalThis, "clearInterval"); const store = createInMemoryResumableStreamStore({ gcIntervalMs: 50 }); store.dispose(); expect(clearSpy).toHaveBeenCalledTimes(1); clearSpy.mockRestore(); } finally { vi.useRealTimers(); } }); it("dispose is a no-op when gcIntervalMs is undefined", () => { const store = createInMemoryResumableStreamStore(); expect(() => store.dispose()).not.toThrow(); }); it("keeps a superseded producer out of a stream reacquired on the same instance", async () => { let now = 0; const store = createInMemoryResumableStreamStore({ now: () => now, defaultTtlMs: 10, }); const a = await store.acquireLease!("s"); if (a.role !== "producer") throw new Error("Expected producer"); await store.append("s", bytes("before"), a.lease); now = 11; const b = await store.acquireLease!("s"); if (b.role !== "producer") throw new Error("Expected producer"); await expect(store.acquireLease!("s")).resolves.toEqual({ role: "consumer", }); await store.append("s", bytes("fresh"), b.lease); await expect( store.append("s", bytes("stale"), a.lease), ).rejects.toMatchObject({ code: "missing", message: "Stream superseded by a new acquisition: s", }); await expect(store.finalize("s", "done", undefined, a.lease)).resolves.toBe( false, ); await expect(store.status("s")).resolves.toBe("streaming"); await expect(store.finalize("s", "done", undefined, b.lease)).resolves.toBe( true, ); await expect(store.finalize("s", "done", undefined, b.lease)).resolves.toBe( false, ); await expect( store.append("s", bytes("late"), a.lease), ).rejects.toMatchObject({ code: "missing", }); const chunks: string[] = []; for await (const entry of store.read( "s", "", new AbortController().signal, )) { chunks.push(decode(entry.chunk)); } expect(chunks).toEqual(["fresh"]); await store.delete("s"); await expect(store.finalize("s", "done")).rejects.toThrow(/not found/); }); });