assistant-stream 0.3.13 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +39 -0
  2. package/dist/core/AssistantStreamChunk.d.ts +2 -0
  3. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  4. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  5. package/dist/core/accumulators/assistant-message-accumulator.js +3 -0
  6. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  7. package/dist/core/modules/tool-call.d.ts.map +1 -1
  8. package/dist/core/modules/tool-call.js +3 -0
  9. package/dist/core/modules/tool-call.js.map +1 -1
  10. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  11. package/dist/core/tool/ToolExecutionStream.js +3 -0
  12. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  13. package/dist/core/tool/ToolResponse.d.ts +3 -0
  14. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  15. package/dist/core/tool/ToolResponse.js +4 -0
  16. package/dist/core/tool/ToolResponse.js.map +1 -1
  17. package/dist/core/tool/tool-types.d.ts +17 -0
  18. package/dist/core/tool/tool-types.d.ts.map +1 -1
  19. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  20. package/dist/core/tool/toolResultStream.js +26 -1
  21. package/dist/core/tool/toolResultStream.js.map +1 -1
  22. package/dist/core/utils/types.d.ts +4 -0
  23. package/dist/core/utils/types.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/resumable/ResumableStreamContext.d.ts +27 -0
  28. package/dist/resumable/ResumableStreamContext.d.ts.map +1 -0
  29. package/dist/resumable/ResumableStreamContext.js +121 -0
  30. package/dist/resumable/ResumableStreamContext.js.map +1 -0
  31. package/dist/resumable/constants.d.ts +2 -0
  32. package/dist/resumable/constants.d.ts.map +1 -0
  33. package/dist/resumable/constants.js +2 -0
  34. package/dist/resumable/constants.js.map +1 -0
  35. package/dist/resumable/createResumableAssistantStreamResponse.d.ts +24 -0
  36. package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -0
  37. package/dist/resumable/createResumableAssistantStreamResponse.js +40 -0
  38. package/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -0
  39. package/dist/resumable/errors.d.ts +7 -0
  40. package/dist/resumable/errors.d.ts.map +1 -0
  41. package/dist/resumable/errors.js +15 -0
  42. package/dist/resumable/errors.js.map +1 -0
  43. package/dist/resumable/index.d.ts +7 -0
  44. package/dist/resumable/index.d.ts.map +1 -0
  45. package/dist/resumable/index.js +5 -0
  46. package/dist/resumable/index.js.map +1 -0
  47. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +13 -0
  48. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -0
  49. package/dist/resumable/stores/InMemoryResumableStreamStore.js +199 -0
  50. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -0
  51. package/dist/resumable/stores/ioredis.d.ts +10 -0
  52. package/dist/resumable/stores/ioredis.d.ts.map +1 -0
  53. package/dist/resumable/stores/ioredis.js +95 -0
  54. package/dist/resumable/stores/ioredis.js.map +1 -0
  55. package/dist/resumable/stores/redis-impl.d.ts +60 -0
  56. package/dist/resumable/stores/redis-impl.d.ts.map +1 -0
  57. package/dist/resumable/stores/redis-impl.js +198 -0
  58. package/dist/resumable/stores/redis-impl.js.map +1 -0
  59. package/dist/resumable/stores/redis.d.ts +39 -0
  60. package/dist/resumable/stores/redis.d.ts.map +1 -0
  61. package/dist/resumable/stores/redis.js +113 -0
  62. package/dist/resumable/stores/redis.js.map +1 -0
  63. package/dist/resumable/types.d.ts +30 -0
  64. package/dist/resumable/types.d.ts.map +1 -0
  65. package/dist/resumable/types.js +2 -0
  66. package/dist/resumable/types.js.map +1 -0
  67. package/package.json +28 -3
  68. package/src/core/AssistantStreamChunk.ts +2 -0
  69. package/src/core/accumulators/assistant-message-accumulator.ts +3 -0
  70. package/src/core/modules/tool-call.ts +3 -0
  71. package/src/core/tool/ToolExecutionStream.ts +3 -0
  72. package/src/core/tool/ToolResponse.ts +6 -0
  73. package/src/core/tool/tool-types.ts +23 -0
  74. package/src/core/tool/toolResultStream.test.ts +360 -2
  75. package/src/core/tool/toolResultStream.ts +30 -1
  76. package/src/core/utils/types.ts +4 -0
  77. package/src/index.ts +5 -1
  78. package/src/resumable/ResumableStreamContext.test.ts +274 -0
  79. package/src/resumable/ResumableStreamContext.ts +187 -0
  80. package/src/resumable/__tests__/integration.test.ts +159 -0
  81. package/src/resumable/constants.ts +1 -0
  82. package/src/resumable/createResumableAssistantStreamResponse.test.ts +243 -0
  83. package/src/resumable/createResumableAssistantStreamResponse.ts +80 -0
  84. package/src/resumable/errors.ts +26 -0
  85. package/src/resumable/index.ts +36 -0
  86. package/src/resumable/stores/InMemoryResumableStreamStore.test.ts +285 -0
  87. package/src/resumable/stores/InMemoryResumableStreamStore.ts +237 -0
  88. package/src/resumable/stores/ioredis.ts +123 -0
  89. package/src/resumable/stores/redis-impl.ts +304 -0
  90. package/src/resumable/stores/redis.test.ts +265 -0
  91. package/src/resumable/stores/redis.ts +171 -0
  92. package/src/resumable/types.ts +49 -0
@@ -0,0 +1,265 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { createClient, type RedisClientType } from "redis";
3
+ import IoRedis from "ioredis";
4
+ import { createRedisResumableStreamStore } from "./redis";
5
+ import { createIoredisResumableStreamStore } from "./ioredis";
6
+ import type { ResumableStreamStore } from "../types";
7
+
8
+ // Redis adapter tests are skipped unless a reachable Redis is signaled via
9
+ // REDIS_URL or REDIS_TESTS=1. CI without a redis service is the default.
10
+ const REDIS_URL = process.env["REDIS_URL"] ?? "redis://127.0.0.1:6379";
11
+ const REDIS_TESTS_DISABLED =
12
+ process.env["REDIS_URL"] === undefined && process.env["REDIS_TESTS"] !== "1";
13
+
14
+ const enc = new TextEncoder();
15
+ const dec = new TextDecoder();
16
+ const bytes = (s: string): Uint8Array => enc.encode(s);
17
+ const text = (b: Uint8Array): string => dec.decode(b);
18
+
19
+ const KEY_PREFIX_BASE = `aui:resumable:test:${Date.now()}`;
20
+ let suiteCounter = 0;
21
+
22
+ type Adapter = {
23
+ name: string;
24
+ setup: () => Promise<{
25
+ store: ResumableStreamStore;
26
+ keyPrefix: string;
27
+ makeStore: (
28
+ overrides?: Partial<{
29
+ maxChunkBytes: number;
30
+ }>,
31
+ ) => ResumableStreamStore;
32
+ cleanup: () => Promise<void>;
33
+ }>;
34
+ };
35
+
36
+ const adapters: Adapter[] = [
37
+ {
38
+ name: "node-redis",
39
+ setup: async () => {
40
+ const client: RedisClientType = createClient({ url: REDIS_URL });
41
+ client.on("error", () => {});
42
+ await client.connect();
43
+ const keyPrefix = `${KEY_PREFIX_BASE}:nr:${suiteCounter++}`;
44
+ const makeStore = (overrides?: { maxChunkBytes?: number }) =>
45
+ createRedisResumableStreamStore(client, {
46
+ keyPrefix,
47
+ pollIntervalMs: 25,
48
+ ...overrides,
49
+ });
50
+ return {
51
+ store: makeStore(),
52
+ keyPrefix,
53
+ makeStore,
54
+ cleanup: async () => {
55
+ const keys = await client.keys(`${keyPrefix}:*`);
56
+ if (keys.length > 0) await client.del(keys);
57
+ await client.quit();
58
+ },
59
+ };
60
+ },
61
+ },
62
+ {
63
+ name: "ioredis",
64
+ setup: async () => {
65
+ const client = new IoRedis(REDIS_URL, { lazyConnect: true });
66
+ await client.connect();
67
+ const keyPrefix = `${KEY_PREFIX_BASE}:io:${suiteCounter++}`;
68
+ const makeStore = (overrides?: { maxChunkBytes?: number }) =>
69
+ createIoredisResumableStreamStore(client, {
70
+ keyPrefix,
71
+ pollIntervalMs: 25,
72
+ ...overrides,
73
+ });
74
+ return {
75
+ store: makeStore(),
76
+ keyPrefix,
77
+ makeStore,
78
+ cleanup: async () => {
79
+ const keys = await client.keys(`${keyPrefix}:*`);
80
+ if (keys.length > 0) await client.del(...keys);
81
+ await client.quit();
82
+ },
83
+ };
84
+ },
85
+ },
86
+ ];
87
+
88
+ for (const adapter of adapters) {
89
+ describe.skipIf(REDIS_TESTS_DISABLED)(
90
+ `Redis adapter: ${adapter.name}`,
91
+ () => {
92
+ let store: ResumableStreamStore;
93
+ let makeStore: (overrides?: {
94
+ maxChunkBytes?: number;
95
+ }) => ResumableStreamStore;
96
+ let cleanup: () => Promise<void>;
97
+
98
+ beforeAll(async () => {
99
+ const ctx = await adapter.setup();
100
+ store = ctx.store;
101
+ makeStore = ctx.makeStore;
102
+ cleanup = ctx.cleanup;
103
+ });
104
+
105
+ afterAll(async () => {
106
+ await cleanup();
107
+ });
108
+
109
+ it("elects exactly one producer per id", async () => {
110
+ const id = `id-${Math.random()}`;
111
+ const first = await store.acquire(id);
112
+ const second = await store.acquire(id);
113
+ expect(first).toBe("producer");
114
+ expect(second).toBe("consumer");
115
+ });
116
+
117
+ it("replays buffered chunks and tails until done", async () => {
118
+ const id = `id-${Math.random()}`;
119
+ await store.acquire(id);
120
+ await store.append(id, bytes("hello"));
121
+ await store.append(id, bytes(" world"));
122
+
123
+ const ac = new AbortController();
124
+ const collected: string[] = [];
125
+ const reading = (async () => {
126
+ for await (const entry of store.read(id, "", ac.signal)) {
127
+ collected.push(text(entry.chunk));
128
+ if (collected.length === 3) {
129
+ await store.finalize(id, "done");
130
+ }
131
+ }
132
+ })();
133
+
134
+ await new Promise((r) => setTimeout(r, 50));
135
+ await store.append(id, bytes("!"));
136
+ await reading;
137
+ expect(collected.join("")).toBe("hello world!");
138
+ });
139
+
140
+ it("status: missing → streaming → done", async () => {
141
+ const id = `id-${Math.random()}`;
142
+ expect(await store.status(id)).toBe("missing");
143
+ await store.acquire(id);
144
+ expect(await store.status(id)).toBe("streaming");
145
+ await store.finalize(id, "done");
146
+ expect(await store.status(id)).toBe("done");
147
+ });
148
+
149
+ it("status: error finalize", async () => {
150
+ const id = `id-${Math.random()}`;
151
+ await store.acquire(id);
152
+ await store.finalize(id, "error", "boom");
153
+ expect(await store.status(id)).toBe("error");
154
+ });
155
+
156
+ it("read throws on error finalize after draining buffered entries", async () => {
157
+ const id = `id-${Math.random()}`;
158
+ await store.acquire(id);
159
+ await store.append(id, bytes("partial"));
160
+ await store.finalize(id, "error", "boom");
161
+
162
+ const ac = new AbortController();
163
+ const seen: string[] = [];
164
+ await expect(async () => {
165
+ for await (const entry of store.read(id, "", ac.signal)) {
166
+ seen.push(text(entry.chunk));
167
+ }
168
+ }).rejects.toThrow("boom");
169
+ expect(seen).toEqual(["partial"]);
170
+ });
171
+
172
+ it("late consumer replays after done", async () => {
173
+ const id = `id-${Math.random()}`;
174
+ await store.acquire(id);
175
+ await store.append(id, bytes("a"));
176
+ await store.append(id, bytes("b"));
177
+ await store.append(id, bytes("c"));
178
+ await store.finalize(id, "done");
179
+
180
+ const ac = new AbortController();
181
+ const out: string[] = [];
182
+ for await (const entry of store.read(id, "", ac.signal)) {
183
+ out.push(text(entry.chunk));
184
+ }
185
+ expect(out).toEqual(["a", "b", "c"]);
186
+ });
187
+
188
+ it("cursor advances and skips already-seen entries", async () => {
189
+ const id = `id-${Math.random()}`;
190
+ await store.acquire(id);
191
+ await store.append(id, bytes("1"));
192
+ await store.append(id, bytes("2"));
193
+ await store.append(id, bytes("3"));
194
+ await store.finalize(id, "done");
195
+
196
+ const ac = new AbortController();
197
+ const seen: { cursor: string; text: string }[] = [];
198
+ for await (const entry of store.read(id, "", ac.signal)) {
199
+ seen.push({ cursor: entry.cursor, text: text(entry.chunk) });
200
+ }
201
+ expect(seen.map((s) => s.text)).toEqual(["1", "2", "3"]);
202
+
203
+ const out: string[] = [];
204
+ for await (const entry of store.read(id, seen[0]!.cursor, ac.signal)) {
205
+ out.push(text(entry.chunk));
206
+ }
207
+ expect(out).toEqual(["2", "3"]);
208
+ });
209
+
210
+ it("delete removes a stream", async () => {
211
+ const id = `id-${Math.random()}`;
212
+ await store.acquire(id);
213
+ await store.append(id, bytes("x"));
214
+ expect(await store.status(id)).toBe("streaming");
215
+ await store.delete(id);
216
+ expect(await store.status(id)).toBe("missing");
217
+ });
218
+
219
+ it("preserves arbitrary binary bytes through pipelined append", async () => {
220
+ const id = `id-${Math.random()}`;
221
+ await store.acquire(id);
222
+
223
+ const producer = new Uint8Array(512);
224
+ for (let i = 0; i < producer.length; i++) producer[i] = i & 0xff;
225
+ const half = producer.length / 2;
226
+ await store.append(id, producer.slice(0, half));
227
+ await store.append(id, producer.slice(half));
228
+ await store.finalize(id, "done");
229
+
230
+ const ac = new AbortController();
231
+ const replayed: Uint8Array[] = [];
232
+ for await (const entry of store.read(id, "", ac.signal)) {
233
+ replayed.push(entry.chunk);
234
+ }
235
+ const total = replayed.reduce((n, c) => n + c.byteLength, 0);
236
+ const flat = new Uint8Array(total);
237
+ let offset = 0;
238
+ for (const c of replayed) {
239
+ flat.set(c, offset);
240
+ offset += c.byteLength;
241
+ }
242
+ expect(flat.byteLength).toBe(producer.byteLength);
243
+ expect(Array.from(flat)).toEqual(Array.from(producer));
244
+ });
245
+
246
+ it("rejects appends larger than maxChunkBytes", async () => {
247
+ const limited = makeStore({ maxChunkBytes: 4 });
248
+ const id = `id-${Math.random()}`;
249
+ await limited.acquire(id);
250
+ await expect(limited.append(id, bytes("12345"))).rejects.toThrow(
251
+ /maxChunkBytes/,
252
+ );
253
+ await limited.append(id, bytes("ok"));
254
+ await limited.finalize(id, "done");
255
+
256
+ const ac = new AbortController();
257
+ const out: string[] = [];
258
+ for await (const entry of limited.read(id, "", ac.signal)) {
259
+ out.push(text(entry.chunk));
260
+ }
261
+ expect(out).toEqual(["ok"]);
262
+ });
263
+ },
264
+ );
265
+ }
@@ -0,0 +1,171 @@
1
+ import {
2
+ RedisResumableStreamStore,
3
+ type PipelineCommand,
4
+ type RedisLikeClient,
5
+ type RedisResumableStreamStoreOptions,
6
+ } from "./redis-impl";
7
+ import type { ResumableStreamStore } from "../types";
8
+
9
+ const RESP_BLOB_STRING = 36;
10
+
11
+ type NodeRedisFields = Record<string, string | Buffer>;
12
+
13
+ interface NodeRedisMultiCommand {
14
+ xAdd(key: string, id: string, fields: NodeRedisFields): NodeRedisMultiCommand;
15
+ expire(key: string, seconds: number): NodeRedisMultiCommand;
16
+ set(
17
+ key: string,
18
+ value: string,
19
+ options: { EX: number },
20
+ ): NodeRedisMultiCommand;
21
+ execAsPipeline(): Promise<unknown>;
22
+ exec(): Promise<unknown>;
23
+ }
24
+
25
+ /** Structural subset of node-redis v5 used by the adapter. */
26
+ export interface NodeRedisLike {
27
+ set(
28
+ key: string,
29
+ value: string,
30
+ options: { NX: true; EX: number },
31
+ ): Promise<string | null>;
32
+ set(
33
+ key: string,
34
+ value: string,
35
+ options: { EX: number },
36
+ ): Promise<string | null>;
37
+ get(key: string): Promise<string | null>;
38
+ expire(key: string, seconds: number): Promise<unknown>;
39
+ exists(key: string): Promise<number>;
40
+ del(keys: string | string[]): Promise<unknown>;
41
+ xAdd(key: string, id: string, fields: NodeRedisFields): Promise<string>;
42
+ sendCommand<T = unknown>(
43
+ args: ReadonlyArray<string | Buffer>,
44
+ options?: { typeMapping?: Record<number, unknown> },
45
+ ): Promise<T>;
46
+ multi(): NodeRedisMultiCommand;
47
+ }
48
+
49
+ /**
50
+ * Resumable stream store backed by [`redis`](https://www.npmjs.com/package/redis)
51
+ * v5. Expects a connected client; cluster routing relies on the shared
52
+ * `{streamId}` hash tag baked into the key scheme.
53
+ */
54
+ export function createRedisResumableStreamStore(
55
+ client: NodeRedisLike,
56
+ options?: RedisResumableStreamStoreOptions,
57
+ ): ResumableStreamStore {
58
+ return new RedisResumableStreamStore(adapt(client), options);
59
+ }
60
+
61
+ function adapt(client: NodeRedisLike): RedisLikeClient {
62
+ return {
63
+ async setNX(key, value, ttlSec) {
64
+ const result = await client.set(key, value, { NX: true, EX: ttlSec });
65
+ return result === "OK";
66
+ },
67
+ async set(key, value, ttlSec) {
68
+ await client.set(key, value, { EX: ttlSec });
69
+ },
70
+ async get(key) {
71
+ return client.get(key);
72
+ },
73
+ async expire(key, ttlSec) {
74
+ await client.expire(key, ttlSec);
75
+ },
76
+ async exists(key) {
77
+ const result = await client.exists(key);
78
+ return result > 0;
79
+ },
80
+ async del(keys) {
81
+ if (keys.length === 0) return;
82
+ await client.del(keys.length === 1 ? keys[0]! : keys);
83
+ },
84
+ async xAdd(key, fields) {
85
+ return client.xAdd(key, "*", toNodeFields(fields));
86
+ },
87
+ async xRange(key, start, end) {
88
+ const reply = await client.sendCommand<unknown>(
89
+ ["XRANGE", key, start, end],
90
+ { typeMapping: { [RESP_BLOB_STRING]: Buffer } },
91
+ );
92
+ return parseXRangeReply(reply);
93
+ },
94
+ async pipeline(commands) {
95
+ if (commands.length === 0) return;
96
+ let chain = client.multi();
97
+ for (const cmd of commands) {
98
+ chain = applyPipelineCommand(chain, cmd);
99
+ }
100
+ await chain.execAsPipeline();
101
+ },
102
+ };
103
+ }
104
+
105
+ function applyPipelineCommand(
106
+ chain: NodeRedisMultiCommand,
107
+ cmd: PipelineCommand,
108
+ ): NodeRedisMultiCommand {
109
+ switch (cmd.type) {
110
+ case "xAdd":
111
+ return chain.xAdd(cmd.key, "*", toNodeFields(cmd.fields));
112
+ case "expire":
113
+ return chain.expire(cmd.key, cmd.ttlSec);
114
+ case "set":
115
+ return chain.set(cmd.key, cmd.value, { EX: cmd.ttlSec });
116
+ }
117
+ }
118
+
119
+ function toNodeFields(
120
+ fields: Record<string, string | Uint8Array>,
121
+ ): NodeRedisFields {
122
+ const out: NodeRedisFields = {};
123
+ for (const [k, v] of Object.entries(fields)) {
124
+ out[k] = typeof v === "string" ? v : toBuffer(v);
125
+ }
126
+ return out;
127
+ }
128
+
129
+ function toBuffer(bytes: Uint8Array): Buffer {
130
+ if (Buffer.isBuffer(bytes)) return bytes;
131
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
132
+ }
133
+
134
+ function parseXRangeReply(
135
+ reply: unknown,
136
+ ): Array<{ id: string; fields: Record<string, string | Uint8Array> }> {
137
+ if (!Array.isArray(reply)) return [];
138
+ const out: Array<{
139
+ id: string;
140
+ fields: Record<string, string | Uint8Array>;
141
+ }> = [];
142
+ for (const entry of reply) {
143
+ if (!Array.isArray(entry) || entry.length < 2) continue;
144
+ const [rawId, rawFields] = entry as [unknown, unknown];
145
+ const id = bufferOrStringToString(rawId);
146
+ if (id === undefined || !Array.isArray(rawFields)) continue;
147
+ const fields: Record<string, string | Uint8Array> = {};
148
+ for (let i = 0; i + 1 < rawFields.length; i += 2) {
149
+ const fieldKey = bufferOrStringToString(rawFields[i]);
150
+ const fieldValue = rawFields[i + 1];
151
+ if (fieldKey === undefined || fieldValue === undefined) continue;
152
+ fields[fieldKey] = bufferOrStringToBytes(fieldValue);
153
+ }
154
+ out.push({ id, fields });
155
+ }
156
+ return out;
157
+ }
158
+
159
+ function bufferOrStringToString(value: unknown): string | undefined {
160
+ if (typeof value === "string") return value;
161
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
162
+ return undefined;
163
+ }
164
+
165
+ function bufferOrStringToBytes(value: unknown): string | Uint8Array {
166
+ if (typeof value === "string") return value;
167
+ if (Buffer.isBuffer(value)) {
168
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
169
+ }
170
+ return "";
171
+ }
@@ -0,0 +1,49 @@
1
+ export type ResumableStreamRole = "producer" | "consumer";
2
+
3
+ export type ResumableStreamStatus = "streaming" | "done" | "error" | "missing";
4
+
5
+ export type ResumableStreamEntry = {
6
+ readonly cursor: string;
7
+ readonly chunk: Uint8Array;
8
+ };
9
+
10
+ export type ResumableStreamAcquireOptions = {
11
+ readonly ttlMs?: number;
12
+ };
13
+
14
+ export interface ResumableStreamStore {
15
+ /**
16
+ * Atomic election. The first caller for a given `streamId` observes
17
+ * `"producer"`; every later caller observes `"consumer"`, including those
18
+ * arriving after `finalize`.
19
+ */
20
+ acquire(
21
+ streamId: string,
22
+ options?: ResumableStreamAcquireOptions,
23
+ ): Promise<ResumableStreamRole>;
24
+
25
+ /** Implementations should refresh the TTL on each call. */
26
+ append(streamId: string, chunk: Uint8Array): Promise<void>;
27
+
28
+ finalize(
29
+ streamId: string,
30
+ status: "done" | "error",
31
+ error?: string,
32
+ ): Promise<void>;
33
+
34
+ /**
35
+ * Yields persisted entries strictly after `cursor` (`""` starts from the
36
+ * beginning), then waits for new ones until the stream is finalized.
37
+ * Aborting `signal` resolves the iterable without throwing.
38
+ */
39
+ read(
40
+ streamId: string,
41
+ cursor: string,
42
+ signal: AbortSignal,
43
+ ): AsyncIterable<ResumableStreamEntry>;
44
+
45
+ status(streamId: string): Promise<ResumableStreamStatus>;
46
+
47
+ /** Active readers terminate. No-op when the stream does not exist. */
48
+ delete(streamId: string): Promise<void>;
49
+ }