assistant-stream
Version:
Streaming utilities for AI assistants
716 lines (641 loc) • 24.6 kB
text/typescript
import { describe, expect, it } from "vitest";
import {
RedisResumableStreamStore,
type RedisAppendOptions,
type RedisDeleteOptions,
type RedisFinalizeOptions,
type PipelineCommand,
type RedisLikeClient,
} from "./redis-impl";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
class FakeRedisClient implements RedisLikeClient {
readonly strings = new Map<string, string>();
readonly streams = new Map<
string,
Array<{
id: string;
fields: Record<string, string | Uint8Array>;
}>
>();
onFirstAcquire: (() => Promise<void>) | undefined;
onNextGet: (() => Promise<void>) | undefined;
private nextStreamId = 0;
async setNX(key: string, value: string): Promise<boolean> {
if (this.strings.has(key)) return false;
this.strings.set(key, value);
const onFirstAcquire = this.onFirstAcquire;
this.onFirstAcquire = undefined;
await onFirstAcquire?.();
return true;
}
private setString(key: string, value: string): void {
this.strings.set(key, value);
}
async get(key: string): Promise<string | null> {
const value = this.strings.get(key) ?? null;
const onNextGet = this.onNextGet;
this.onNextGet = undefined;
await onNextGet?.();
return value;
}
async del(keys: string[]): Promise<void> {
for (const key of keys) {
this.strings.delete(key);
this.streams.delete(key);
}
}
async xAdd(
key: string,
fields: Record<string, string | Uint8Array>,
): Promise<string> {
const id = `${++this.nextStreamId}-0`;
const entries = this.streams.get(key) ?? [];
entries.push({ id, fields });
this.streams.set(key, entries);
return id;
}
async xRange(
key: string,
start: string,
): Promise<
Array<{
id: string;
fields: Record<string, string | Uint8Array>;
}>
> {
const entries = this.streams.get(key) ?? [];
if (start === "-") return entries;
const exclusiveId = start.startsWith("(") ? start.slice(1) : start;
const sequence = (id: string) => Number(id.split("-")[0]);
return entries.filter(
(entry) => sequence(entry.id) > sequence(exclusiveId),
);
}
async pipeline(commands: readonly PipelineCommand[]): Promise<void> {
for (const command of commands) {
switch (command.type) {
case "xAdd":
await this.xAdd(command.key, command.fields);
break;
case "expire":
break;
}
}
}
async appendIfUnchanged(options: RedisAppendOptions): Promise<boolean> {
if (this.strings.get(options.metaKey) !== options.expectedMeta)
return false;
await this.xAdd(options.dataKey, options.fields);
return true;
}
async finalizeIfUnchanged(options: RedisFinalizeOptions): Promise<boolean> {
if (this.strings.get(options.metaKey) !== options.expectedMeta)
return false;
await this.xAdd(options.dataKey, options.fields);
this.setString(options.metaKey, options.nextMeta);
return true;
}
async deleteIfUnchanged(options: RedisDeleteOptions): Promise<boolean> {
if (this.strings.get(options.metaKey) !== options.expectedMeta)
return false;
await this.del([options.metaKey, ...options.dataKeys]);
return true;
}
}
const withoutFencedMutations = (client: FakeRedisClient): RedisLikeClient => ({
setNX: (key, value) => client.setNX(key, value),
get: (...args) => client.get(...args),
del: (...args) => client.del(...args),
xRange: (key, start) => client.xRange(key, start),
pipeline: (...args) => client.pipeline(...args),
finalizeIfUnchanged: (...args) => client.finalizeIfUnchanged(...args),
});
describe("RedisResumableStreamStore", () => {
it("does not expose stale data while a stream id is reacquired", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "stream";
const legacyDataKey = `${keyPrefix}:{${streamId}}:data`;
await client.xAdd(legacyDataKey, { c: encoder.encode("stale") });
const store = new RedisResumableStreamStore(client, {
keyPrefix,
pollIntervalMs: 1,
});
let firstChunk: string | undefined;
client.onFirstAcquire = async () => {
await expect(store.acquire(streamId)).resolves.toBe("consumer");
await store.append(streamId, encoder.encode("fresh"));
const abort = new AbortController();
const iterator = store
.read(streamId, "", abort.signal)
[Symbol.asyncIterator]();
const result = await iterator.next();
firstChunk = result.done ? undefined : decoder.decode(result.value.chunk);
abort.abort();
await iterator.return?.();
};
await expect(store.acquire(streamId)).resolves.toBe("producer");
expect(firstChunk).toBe("fresh");
});
it("continues reading legacy streams without a generation", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "legacy";
client.strings.set(
`${keyPrefix}:{${streamId}}:meta`,
JSON.stringify({ status: "streaming", ttlSec: 60 }),
);
const store = new RedisResumableStreamStore(client, { keyPrefix });
await store.append(streamId, encoder.encode("legacy"));
await store.finalize(streamId, "done");
const chunks: string[] = [];
for await (const entry of store.read(
streamId,
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["legacy"]);
});
it("supports custom clients without fenced mutation capabilities", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "custom-client";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const legacyDataKey = `${keyPrefix}:{${streamId}}:data`;
const store = new RedisResumableStreamStore(
withoutFencedMutations(client),
{ keyPrefix },
);
await store.acquire(streamId);
const generation = (
JSON.parse(client.strings.get(metaKey)!) as { generation: string }
).generation;
const dataKey = `${keyPrefix}:{${streamId}}:data:${generation}`;
await client.xAdd(legacyDataKey, { c: encoder.encode("legacy") });
await store.append(streamId, encoder.encode("current"));
expect(client.streams.get(dataKey)?.[0]?.fields.c).toEqual(
encoder.encode("current"),
);
await store.delete(streamId);
expect(client.strings.has(metaKey)).toBe(false);
expect(client.streams.has(dataKey)).toBe(false);
expect(client.streams.has(legacyDataKey)).toBe(false);
});
it("finalizes and replays generation-scoped streams", async () => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
await store.acquire("current");
await store.append("current", encoder.encode("current"));
await store.finalize("current", "done");
const chunks: string[] = [];
for await (const entry of store.read(
"current",
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["current"]);
await expect(store.status("current")).resolves.toBe("done");
});
it("does not resurrect a stream whose metadata expired mid-finalize", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "expired";
const store = new RedisResumableStreamStore(client, { keyPrefix });
await store.acquire(streamId);
let resumeFinalizer!: () => void;
const finalizerPaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeFinalizer = resume;
resolve();
});
});
const finalizing = store.finalize(streamId, "done");
await finalizerPaused;
client.strings.delete(`${keyPrefix}:{${streamId}}:meta`);
resumeFinalizer();
await finalizing;
await expect(store.status(streamId)).resolves.toBe("missing");
});
it.each(["done", "error"] as const)(
"releases an expired producer's lease after %s finalization",
async (status) => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
const generations = Reflect.get(store, "acquiredGenerations") as Map<
string,
string
>;
const acquisition = await store.acquireLease("expired");
if (acquisition.role !== "producer") throw new Error("Expected producer");
await store.acquireLease("active");
client.strings.delete("test:{expired}:meta");
await expect(
store.finalize("expired", status, undefined, acquisition.lease),
).rejects.toThrow("Stream not found: expired");
expect([...generations.keys()]).toEqual(["active"]);
await expect(store.status("expired")).resolves.toBe("missing");
},
);
it.each(["expired", "superseded", "finalized"] as const)(
"releases a leased producer when its stream is %s during finalization",
async (outcome) => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
const generations = Reflect.get(store, "acquiredGenerations") as Map<
string,
string
>;
const acquisition = await store.acquireLease("stream");
if (acquisition.role !== "producer") throw new Error("Expected producer");
let resumeFinalizer!: () => void;
const paused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeFinalizer = resume;
resolve();
});
});
const finalizing = store.finalize(
"stream",
"done",
undefined,
acquisition.lease,
);
await paused;
const other = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
if (outcome === "finalized") {
await other.finalize("stream", "done");
} else {
client.strings.delete("test:{stream}:meta");
if (outcome === "superseded") await other.acquire("stream");
}
resumeFinalizer();
await expect(finalizing).resolves.toBe(false);
expect(generations.size).toBe(0);
if (outcome === "superseded") {
await expect(
store.append("stream", encoder.encode("stale"), acquisition.lease),
).rejects.toMatchObject({ code: "missing" });
await expect(other.status("stream")).resolves.toBe("streaming");
}
},
);
it.each(["superseded", "finalized"] as const)(
"releases a lease when finalization finds the stream already %s",
async (outcome) => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
const generations = Reflect.get(store, "acquiredGenerations") as Map<
string,
string
>;
const acquisition = await store.acquireLease("stream");
if (acquisition.role !== "producer") throw new Error("Expected producer");
const other = new RedisResumableStreamStore(client, {
keyPrefix: "test",
});
if (outcome === "superseded") {
client.strings.delete("test:{stream}:meta");
await other.acquire("stream");
} else {
await other.finalize("stream", "done");
}
await expect(
store.finalize("stream", "done", undefined, acquisition.lease),
).resolves.toBe(false);
expect(generations.size).toBe(0);
},
);
it("keeps a newer local lease when an expired finalizer resumes", async () => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, { keyPrefix: "test" });
const generations = Reflect.get(store, "acquiredGenerations") as Map<
string,
string
>;
const stale = await store.acquireLease("stream");
if (stale.role !== "producer") throw new Error("Expected producer");
client.strings.delete("test:{stream}:meta");
let resumeFinalizer!: () => void;
const paused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeFinalizer = resume;
resolve();
});
});
const finalizing = store.finalize("stream", "done", undefined, stale.lease);
const rejected = expect(finalizing).rejects.toThrow("Stream not found");
await paused;
const fresh = await store.acquireLease("stream");
if (fresh.role !== "producer") throw new Error("Expected producer");
resumeFinalizer();
await rejected;
expect(generations.get("stream")).toBe(fresh.lease.token);
await expect(
store.append("stream", encoder.encode("fresh"), fresh.lease),
).resolves.toBeUndefined();
await expect(
store.append("stream", encoder.encode("stale"), stale.lease),
).rejects.toMatchObject({ code: "missing" });
});
it("keeps the lease-less ownership fence after an expired finalization", async () => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, { keyPrefix: "test" });
await store.acquire("stream");
client.strings.delete("test:{stream}:meta");
await expect(store.finalize("stream", "done")).rejects.toThrow(
"Stream not found",
);
const other = new RedisResumableStreamStore(client, { keyPrefix: "test" });
await other.acquire("stream");
await expect(
store.append("stream", encoder.encode("stale")),
).rejects.toMatchObject({ code: "missing" });
await expect(other.status("stream")).resolves.toBe("streaming");
});
it("fences a superseded producer out of the reacquired stream", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "fenced";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const staleStore = new RedisResumableStreamStore(client, { keyPrefix });
await expect(staleStore.acquire(streamId)).resolves.toBe("producer");
await staleStore.append(streamId, encoder.encode("before"));
client.strings.delete(metaKey);
const freshStore = new RedisResumableStreamStore(client, { keyPrefix });
await expect(freshStore.acquire(streamId)).resolves.toBe("producer");
await freshStore.append(streamId, encoder.encode("fresh"));
await expect(
staleStore.append(streamId, encoder.encode("stale")),
).rejects.toThrow(`Stream superseded by a new acquisition: ${streamId}`);
await expect(staleStore.finalize(streamId, "done")).resolves.toBe(false);
await expect(freshStore.status(streamId)).resolves.toBe("streaming");
await expect(freshStore.finalize(streamId, "done")).resolves.toBe(true);
const chunks: string[] = [];
for await (const entry of freshStore.read(
streamId,
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["fresh"]);
});
it("does not let a stale finalizer overwrite a reacquired stream", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "finalize-race";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const staleStore = new RedisResumableStreamStore(client, { keyPrefix });
const freshStore = new RedisResumableStreamStore(client, { keyPrefix });
await staleStore.acquire(streamId);
let resumeFinalizer!: () => void;
const finalizerPaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeFinalizer = resume;
resolve();
});
});
const finalizing = staleStore.finalize(streamId, "done");
await finalizerPaused;
client.strings.delete(metaKey);
await expect(freshStore.acquire(streamId)).resolves.toBe("producer");
resumeFinalizer();
await expect(finalizing).resolves.toBe(false);
await expect(freshStore.status(streamId)).resolves.toBe("streaming");
await expect(
staleStore.append(streamId, encoder.encode("stale")),
).rejects.toThrow(/superseded/);
});
it("does not let an in-flight append mutate a reacquired stream", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "append-race";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const staleStore = new RedisResumableStreamStore(client, { keyPrefix });
const freshStore = new RedisResumableStreamStore(client, { keyPrefix });
await staleStore.acquire(streamId);
let resumeAppend!: () => void;
const appendPaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeAppend = resume;
resolve();
});
});
const appending = staleStore.append(streamId, encoder.encode("stale"));
await appendPaused;
client.strings.delete(metaKey);
await expect(freshStore.acquire(streamId)).resolves.toBe("producer");
resumeAppend();
await expect(appending).rejects.toThrow(/superseded/);
await freshStore.append(streamId, encoder.encode("fresh"));
await freshStore.finalize(streamId, "done");
const chunks: string[] = [];
for await (const entry of freshStore.read(
streamId,
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["fresh"]);
});
it("reports a leased in-flight append on the reacquiring instance as superseded", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "leased-append-race";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const store = new RedisResumableStreamStore(client, { keyPrefix });
const stale = await store.acquireLease(streamId);
if (stale.role !== "producer") throw new Error("Expected producer");
let resumeAppend!: () => void;
const appendPaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeAppend = resume;
resolve();
});
});
const appending = store.append(
streamId,
encoder.encode("stale"),
stale.lease,
);
await appendPaused;
client.strings.delete(metaKey);
const fresh = await store.acquireLease(streamId);
if (fresh.role !== "producer") throw new Error("Expected producer");
resumeAppend();
await expect(appending).rejects.toMatchObject({
code: "missing",
message: `Stream superseded by a new acquisition: ${streamId}`,
});
});
it("does not let an in-flight delete remove a reacquired stream", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "delete-race";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const staleStore = new RedisResumableStreamStore(client, { keyPrefix });
const freshStore = new RedisResumableStreamStore(client, { keyPrefix });
await staleStore.acquire(streamId);
const generation = (
JSON.parse(client.strings.get(metaKey)!) as { generation: string }
).generation;
const staleDataKey = `${keyPrefix}:{${streamId}}:data:${generation}`;
await staleStore.append(streamId, encoder.encode("stale"));
let resumeDelete!: () => void;
const deletePaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeDelete = resume;
resolve();
});
});
const deleting = staleStore.delete(streamId);
await deletePaused;
client.strings.delete(metaKey);
await expect(freshStore.acquire(streamId)).resolves.toBe("producer");
resumeDelete();
await deleting;
await expect(freshStore.status(streamId)).resolves.toBe("streaming");
expect(client.streams.has(staleDataKey)).toBe(false);
await staleStore.delete(streamId);
await expect(freshStore.status(streamId)).resolves.toBe("missing");
});
it("does not clear a same-store acquisition completed during delete", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "same-store-delete-race";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const store = new RedisResumableStreamStore(client, { keyPrefix });
await store.acquire(streamId);
const generation = (
JSON.parse(client.strings.get(metaKey)!) as { generation: string }
).generation;
const staleDataKey = `${keyPrefix}:{${streamId}}:data:${generation}`;
await store.append(streamId, encoder.encode("stale"));
client.strings.delete(metaKey);
let resumeDelete!: () => void;
const deletePaused = new Promise<void>((resolve) => {
client.onNextGet = () =>
new Promise<void>((resume) => {
resumeDelete = resume;
resolve();
});
});
const deleting = store.delete(streamId);
await deletePaused;
await expect(store.acquire(streamId)).resolves.toBe("producer");
await store.append(streamId, encoder.encode("fresh"));
resumeDelete();
await deleting;
expect(client.streams.has(staleDataKey)).toBe(false);
await store.finalize(streamId, "done");
const chunks: string[] = [];
for await (const entry of store.read(
streamId,
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["fresh"]);
});
it("stops an existing reader when the stream generation changes", async () => {
const client = new FakeRedisClient();
const keyPrefix = "test";
const streamId = "reused";
const metaKey = `${keyPrefix}:{${streamId}}:meta`;
const store = new RedisResumableStreamStore(client, {
keyPrefix,
pollIntervalMs: 1,
});
await store.acquire(streamId);
const abort = new AbortController();
const iterator = store
.read(streamId, "", abort.signal)
[Symbol.asyncIterator]();
const next = iterator.next();
await new Promise((resolve) => setTimeout(resolve, 2));
client.strings.delete(metaKey);
await store.acquire(streamId);
let timeout: ReturnType<typeof setTimeout> | undefined;
const result = await Promise.race([
next,
new Promise<"timeout">((resolve) => {
timeout = setTimeout(() => resolve("timeout"), 100);
}),
]);
if (timeout !== undefined) clearTimeout(timeout);
abort.abort();
await iterator.return?.();
expect(result).not.toBe("timeout");
expect(result).toMatchObject({ done: true });
});
it("keeps a superseded producer out of a stream reacquired on the same instance", async () => {
const client = new FakeRedisClient();
const store = new RedisResumableStreamStore(client, { keyPrefix: "test" });
const a = await store.acquireLease("s");
if (a.role !== "producer") throw new Error("Expected producer");
await store.append("s", encoder.encode("before"), a.lease);
client.strings.delete("test:{s}:meta");
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", encoder.encode("fresh"), b.lease);
await expect(
store.append("s", encoder.encode("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", encoder.encode("late"), a.lease),
).rejects.toMatchObject({
code: "missing",
});
const chunks: string[] = [];
for await (const entry of store.read(
"s",
"",
new AbortController().signal,
)) {
chunks.push(decoder.decode(entry.chunk));
}
expect(chunks).toEqual(["fresh"]);
await store.delete("s");
await expect(store.finalize("s", "done")).rejects.toThrow(/not found/);
});
});