@s2-dev/streamstore 0.16.0 → 0.16.1
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.
- package/dist/accessTokens.d.ts +37 -0
- package/dist/accessTokens.d.ts.map +1 -0
- package/dist/accessTokens.js +74 -0
- package/dist/accessTokens.js.map +1 -0
- package/dist/basin.d.ts +26 -0
- package/dist/basin.d.ts.map +1 -0
- package/dist/basin.js +34 -0
- package/dist/basin.js.map +1 -0
- package/dist/basins.d.ts +53 -0
- package/dist/basins.d.ts.map +1 -0
- package/dist/basins.js +115 -0
- package/dist/basins.js.map +1 -0
- package/dist/common.d.ts +44 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +2 -0
- package/dist/common.js.map +1 -0
- package/dist/error.d.ts +28 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +43 -0
- package/dist/error.js.map +1 -0
- package/dist/generated/client/client.gen.d.ts +3 -0
- package/dist/generated/client/client.gen.d.ts.map +1 -0
- package/dist/generated/client/client.gen.js +205 -0
- package/dist/generated/client/client.gen.js.map +1 -0
- package/dist/generated/client/index.d.ts +9 -0
- package/dist/generated/client/index.d.ts.map +1 -0
- package/dist/generated/client/index.js +7 -0
- package/dist/generated/client/index.js.map +1 -0
- package/dist/generated/client/types.gen.d.ts +125 -0
- package/dist/generated/client/types.gen.d.ts.map +1 -0
- package/dist/generated/client/types.gen.js +3 -0
- package/dist/generated/client/types.gen.js.map +1 -0
- package/dist/generated/client/utils.gen.d.ts +34 -0
- package/dist/generated/client/utils.gen.d.ts.map +1 -0
- package/dist/generated/client/utils.gen.js +231 -0
- package/dist/generated/client/utils.gen.js.map +1 -0
- package/{src/generated/client.gen.ts → dist/generated/client.gen.d.ts} +3 -8
- package/dist/generated/client.gen.d.ts.map +1 -0
- package/dist/generated/client.gen.js +6 -0
- package/dist/generated/client.gen.js.map +1 -0
- package/dist/generated/core/auth.gen.d.ts +19 -0
- package/dist/generated/core/auth.gen.d.ts.map +1 -0
- package/dist/generated/core/auth.gen.js +15 -0
- package/dist/generated/core/auth.gen.js.map +1 -0
- package/dist/generated/core/bodySerializer.gen.d.ts +18 -0
- package/dist/generated/core/bodySerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/bodySerializer.gen.js +58 -0
- package/dist/generated/core/bodySerializer.gen.js.map +1 -0
- package/dist/generated/core/params.gen.d.ts +34 -0
- package/dist/generated/core/params.gen.d.ts.map +1 -0
- package/dist/generated/core/params.gen.js +89 -0
- package/dist/generated/core/params.gen.js.map +1 -0
- package/dist/generated/core/pathSerializer.gen.d.ts +34 -0
- package/dist/generated/core/pathSerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/pathSerializer.gen.js +115 -0
- package/dist/generated/core/pathSerializer.gen.js.map +1 -0
- package/dist/generated/core/queryKeySerializer.gen.d.ts +19 -0
- package/dist/generated/core/queryKeySerializer.gen.d.ts.map +1 -0
- package/dist/generated/core/queryKeySerializer.gen.js +100 -0
- package/dist/generated/core/queryKeySerializer.gen.js.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.d.ts +72 -0
- package/dist/generated/core/serverSentEvents.gen.d.ts.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.js +136 -0
- package/dist/generated/core/serverSentEvents.gen.js.map +1 -0
- package/dist/generated/core/types.gen.d.ts +79 -0
- package/dist/generated/core/types.gen.d.ts.map +1 -0
- package/dist/generated/core/types.gen.js +3 -0
- package/dist/generated/core/types.gen.js.map +1 -0
- package/dist/generated/core/utils.gen.d.ts +20 -0
- package/dist/generated/core/utils.gen.d.ts.map +1 -0
- package/dist/generated/core/utils.gen.js +88 -0
- package/dist/generated/core/utils.gen.js.map +1 -0
- package/dist/generated/index.d.ts +3 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/{src/generated/index.ts → dist/generated/index.js} +1 -2
- package/dist/generated/index.js.map +1 -0
- package/dist/generated/sdk.gen.d.ts +100 -0
- package/dist/generated/sdk.gen.d.ts.map +1 -0
- package/dist/generated/sdk.gen.js +350 -0
- package/dist/generated/sdk.gen.js.map +1 -0
- package/{src/generated/types.gen.ts → dist/generated/types.gen.d.ts} +1 -158
- package/dist/generated/types.gen.d.ts.map +1 -0
- package/dist/generated/types.gen.js +3 -0
- package/dist/generated/types.gen.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/event-stream.d.ts +26 -0
- package/dist/lib/event-stream.d.ts.map +1 -0
- package/dist/lib/event-stream.js +138 -0
- package/dist/lib/event-stream.js.map +1 -0
- package/dist/metrics.d.ts +44 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +83 -0
- package/dist/metrics.js.map +1 -0
- package/dist/s2.d.ts +38 -0
- package/dist/s2.d.ts.map +1 -0
- package/dist/s2.js +56 -0
- package/dist/s2.js.map +1 -0
- package/dist/stream.d.ts +156 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +598 -0
- package/dist/stream.js.map +1 -0
- package/dist/streams.d.ts +52 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/streams.js +114 -0
- package/dist/streams.js.map +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +52 -0
- package/dist/utils.js.map +1 -0
- package/package.json +5 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.claude/settings.local.json +0 -9
- package/.github/workflows/ci.yml +0 -59
- package/.github/workflows/publish.yml +0 -35
- package/CHANGELOG.md +0 -7
- package/biome.json +0 -30
- package/bun.lock +0 -598
- package/examples/append.ts +0 -84
- package/examples/kitchen-sink.ts +0 -73
- package/examples/read.ts +0 -30
- package/openapi-ts.config.ts +0 -7
- package/src/accessTokens.ts +0 -100
- package/src/basin.ts +0 -43
- package/src/basins.ts +0 -154
- package/src/common.ts +0 -45
- package/src/error.ts +0 -58
- package/src/generated/client/client.gen.ts +0 -268
- package/src/generated/client/index.ts +0 -26
- package/src/generated/client/types.gen.ts +0 -268
- package/src/generated/client/utils.gen.ts +0 -331
- package/src/generated/core/auth.gen.ts +0 -42
- package/src/generated/core/bodySerializer.gen.ts +0 -92
- package/src/generated/core/params.gen.ts +0 -153
- package/src/generated/core/pathSerializer.gen.ts +0 -181
- package/src/generated/core/queryKeySerializer.gen.ts +0 -136
- package/src/generated/core/serverSentEvents.gen.ts +0 -264
- package/src/generated/core/types.gen.ts +0 -118
- package/src/generated/core/utils.gen.ts +0 -143
- package/src/generated/sdk.gen.ts +0 -387
- package/src/index.ts +0 -66
- package/src/lib/event-stream.ts +0 -167
- package/src/metrics.ts +0 -106
- package/src/s2.ts +0 -65
- package/src/stream.ts +0 -791
- package/src/streams.ts +0 -156
- package/src/tests/appendSession.test.ts +0 -149
- package/src/tests/batcher-session.integration.test.ts +0 -80
- package/src/tests/batcher.test.ts +0 -216
- package/src/tests/index.test.ts +0 -7
- package/src/utils.ts +0 -80
- package/tsconfig.build.json +0 -10
- package/tsconfig.json +0 -31
package/src/streams.ts
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import type { DataToObject, S2RequestOptions } from "./common";
|
|
2
|
-
import { S2Error } from "./error";
|
|
3
|
-
import {
|
|
4
|
-
type CreateStreamData,
|
|
5
|
-
createStream,
|
|
6
|
-
type DeleteStreamData,
|
|
7
|
-
deleteStream,
|
|
8
|
-
type GetStreamConfigData,
|
|
9
|
-
getStreamConfig,
|
|
10
|
-
type ListStreamsData,
|
|
11
|
-
listStreams,
|
|
12
|
-
type ReconfigureStreamData,
|
|
13
|
-
reconfigureStream,
|
|
14
|
-
} from "./generated";
|
|
15
|
-
import type { Client } from "./generated/client/types.gen";
|
|
16
|
-
|
|
17
|
-
export interface ListStreamsArgs extends DataToObject<ListStreamsData> {}
|
|
18
|
-
export interface CreateStreamArgs extends DataToObject<CreateStreamData> {}
|
|
19
|
-
export interface GetStreamConfigArgs
|
|
20
|
-
extends DataToObject<GetStreamConfigData> {}
|
|
21
|
-
export interface DeleteStreamArgs extends DataToObject<DeleteStreamData> {}
|
|
22
|
-
export interface ReconfigureStreamArgs
|
|
23
|
-
extends DataToObject<ReconfigureStreamData> {}
|
|
24
|
-
|
|
25
|
-
export class S2Streams {
|
|
26
|
-
private readonly client: Client;
|
|
27
|
-
constructor(client: Client) {
|
|
28
|
-
this.client = client;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* List streams in the basin.
|
|
33
|
-
*
|
|
34
|
-
* @param args.prefix Return streams whose names start with the given prefix
|
|
35
|
-
* @param args.start_after Name to start after (for pagination)
|
|
36
|
-
* @param args.limit Max results (up to 1000)
|
|
37
|
-
*/
|
|
38
|
-
public async list(args?: ListStreamsArgs, options?: S2RequestOptions) {
|
|
39
|
-
const response = await listStreams({
|
|
40
|
-
client: this.client,
|
|
41
|
-
query: args,
|
|
42
|
-
...options,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
if (response.error) {
|
|
46
|
-
throw new S2Error({
|
|
47
|
-
message: response.error.message,
|
|
48
|
-
code: response.error.code ?? undefined,
|
|
49
|
-
status: response.response.status,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return response.data;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create a stream.
|
|
58
|
-
*
|
|
59
|
-
* @param args.stream Stream name (1-512 bytes, unique within the basin)
|
|
60
|
-
* @param args.config Stream configuration (retention, storage class, timestamping, delete-on-empty)
|
|
61
|
-
*/
|
|
62
|
-
public async create(args: CreateStreamArgs, options?: S2RequestOptions) {
|
|
63
|
-
const response = await createStream({
|
|
64
|
-
client: this.client,
|
|
65
|
-
body: args,
|
|
66
|
-
...options,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (response.error) {
|
|
70
|
-
throw new S2Error({
|
|
71
|
-
message: response.error.message,
|
|
72
|
-
code: response.error.code ?? undefined,
|
|
73
|
-
status: response.response.status,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return response.data;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get stream configuration.
|
|
82
|
-
*
|
|
83
|
-
* @param args.stream Stream name
|
|
84
|
-
*/
|
|
85
|
-
public async getConfig(
|
|
86
|
-
args: GetStreamConfigArgs,
|
|
87
|
-
options?: S2RequestOptions,
|
|
88
|
-
) {
|
|
89
|
-
const response = await getStreamConfig({
|
|
90
|
-
client: this.client,
|
|
91
|
-
path: args,
|
|
92
|
-
...options,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (response.error) {
|
|
96
|
-
throw new S2Error({
|
|
97
|
-
message: response.error.message,
|
|
98
|
-
code: response.error.code ?? undefined,
|
|
99
|
-
status: response.response.status,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return response.data;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Delete a stream.
|
|
108
|
-
*
|
|
109
|
-
* @param args.stream Stream name
|
|
110
|
-
*/
|
|
111
|
-
public async delete(args: DeleteStreamArgs, options?: S2RequestOptions) {
|
|
112
|
-
const response = await deleteStream({
|
|
113
|
-
client: this.client,
|
|
114
|
-
path: args,
|
|
115
|
-
...options,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (response.error) {
|
|
119
|
-
throw new S2Error({
|
|
120
|
-
message: response.error.message,
|
|
121
|
-
code: response.error.code ?? undefined,
|
|
122
|
-
status: response.response.status,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return response.data;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Reconfigure a stream.
|
|
131
|
-
*
|
|
132
|
-
* @param args.stream Stream name
|
|
133
|
-
* @param args.body Configuration fields to change
|
|
134
|
-
*/
|
|
135
|
-
public async reconfigure(
|
|
136
|
-
args: ReconfigureStreamArgs,
|
|
137
|
-
options?: S2RequestOptions,
|
|
138
|
-
) {
|
|
139
|
-
const response = await reconfigureStream({
|
|
140
|
-
client: this.client,
|
|
141
|
-
path: args,
|
|
142
|
-
body: args,
|
|
143
|
-
...options,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (response.error) {
|
|
147
|
-
throw new S2Error({
|
|
148
|
-
message: response.error.message,
|
|
149
|
-
code: response.error.code ?? undefined,
|
|
150
|
-
status: response.response.status,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return response.data;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { AppendAck } from "../generated";
|
|
3
|
-
import type { AppendRecord } from "../stream";
|
|
4
|
-
import { S2Stream } from "../stream";
|
|
5
|
-
|
|
6
|
-
// Minimal Client shape to satisfy S2Stream constructor; we won't use it directly
|
|
7
|
-
const fakeClient: any = {};
|
|
8
|
-
|
|
9
|
-
const makeStream = () => new S2Stream("test-stream", fakeClient);
|
|
10
|
-
|
|
11
|
-
const makeAck = (n: number): AppendAck => ({
|
|
12
|
-
start: { seq_num: n - 1, timestamp: 0 },
|
|
13
|
-
end: { seq_num: n, timestamp: 0 },
|
|
14
|
-
tail: { seq_num: n, timestamp: 0 },
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("AppendSession", () => {
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
vi.useFakeTimers();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
vi.useRealTimers();
|
|
24
|
-
vi.restoreAllMocks();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("serializes submit calls and emits acks in order", async () => {
|
|
28
|
-
const stream = makeStream();
|
|
29
|
-
const appendSpy = vi.spyOn(stream, "append");
|
|
30
|
-
|
|
31
|
-
// ensure only one in flight at a time by controlling resolution of spy
|
|
32
|
-
let firstResolved = false;
|
|
33
|
-
appendSpy.mockImplementationOnce(async (..._args: any[]) => {
|
|
34
|
-
await vi.advanceTimersByTimeAsync(10);
|
|
35
|
-
firstResolved = true;
|
|
36
|
-
return makeAck(1);
|
|
37
|
-
});
|
|
38
|
-
appendSpy.mockImplementationOnce(async (..._args: any[]) => {
|
|
39
|
-
expect(firstResolved).toBe(true);
|
|
40
|
-
await vi.advanceTimersByTimeAsync(5);
|
|
41
|
-
return makeAck(2);
|
|
42
|
-
});
|
|
43
|
-
// default fallback
|
|
44
|
-
appendSpy.mockResolvedValue(makeAck(999));
|
|
45
|
-
|
|
46
|
-
const session = await stream.appendSession();
|
|
47
|
-
|
|
48
|
-
const p1 = session.submit([{ body: "a" }]);
|
|
49
|
-
const p2 = session.submit([{ body: "b" }]);
|
|
50
|
-
|
|
51
|
-
const ack1 = await p1;
|
|
52
|
-
const ack2 = await p2;
|
|
53
|
-
|
|
54
|
-
expect(appendSpy).toHaveBeenCalledTimes(2);
|
|
55
|
-
expect(ack1.end.seq_num).toBe(1);
|
|
56
|
-
expect(ack2.end.seq_num).toBe(2);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("acks() stream receives emitted acks and closes on session.close()", async () => {
|
|
60
|
-
const stream = makeStream();
|
|
61
|
-
const appendSpy = vi
|
|
62
|
-
.spyOn(stream, "append")
|
|
63
|
-
.mockResolvedValueOnce(makeAck(1))
|
|
64
|
-
.mockResolvedValueOnce(makeAck(2));
|
|
65
|
-
|
|
66
|
-
const session = await stream.appendSession();
|
|
67
|
-
const acks = session.acks();
|
|
68
|
-
|
|
69
|
-
const received: AppendAck[] = [];
|
|
70
|
-
const consumer = (async () => {
|
|
71
|
-
for await (const ack of acks) {
|
|
72
|
-
received.push(ack);
|
|
73
|
-
}
|
|
74
|
-
})();
|
|
75
|
-
|
|
76
|
-
await session.submit([{ body: "a" }]);
|
|
77
|
-
await session.submit([{ body: "b" }]);
|
|
78
|
-
|
|
79
|
-
await session.close();
|
|
80
|
-
await consumer;
|
|
81
|
-
|
|
82
|
-
expect(appendSpy).toHaveBeenCalledTimes(2);
|
|
83
|
-
expect(received.map((a) => a.end.seq_num)).toEqual([1, 2]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("close() waits for drain before resolving", async () => {
|
|
87
|
-
const stream = makeStream();
|
|
88
|
-
const appendSpy = vi.spyOn(stream, "append");
|
|
89
|
-
|
|
90
|
-
appendSpy.mockResolvedValueOnce(makeAck(1));
|
|
91
|
-
appendSpy.mockResolvedValueOnce(makeAck(2));
|
|
92
|
-
|
|
93
|
-
const session = await stream.appendSession();
|
|
94
|
-
|
|
95
|
-
const p1 = session.submit([{ body: "x" }]);
|
|
96
|
-
const p2 = session.submit([{ body: "y" }]);
|
|
97
|
-
|
|
98
|
-
await Promise.all([p1, p2]);
|
|
99
|
-
await session.close();
|
|
100
|
-
|
|
101
|
-
await expect(p1).resolves.toBeTruthy();
|
|
102
|
-
await expect(p2).resolves.toBeTruthy();
|
|
103
|
-
expect(appendSpy).toHaveBeenCalledTimes(2);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("submit after close() rejects", async () => {
|
|
107
|
-
const stream = makeStream();
|
|
108
|
-
vi.spyOn(stream, "append").mockResolvedValue(makeAck(1));
|
|
109
|
-
const session = await stream.appendSession();
|
|
110
|
-
|
|
111
|
-
await session.close();
|
|
112
|
-
|
|
113
|
-
await expect(session.submit([{ body: "x" }])).rejects.toMatchObject({
|
|
114
|
-
message: expect.stringContaining("AppendSession is closed"),
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("error during processing rejects current and queued, clears queue", async () => {
|
|
119
|
-
const stream = makeStream();
|
|
120
|
-
const appendSpy = vi.spyOn(stream, "append");
|
|
121
|
-
|
|
122
|
-
appendSpy.mockRejectedValueOnce(new Error("boom"));
|
|
123
|
-
|
|
124
|
-
const session = await stream.appendSession();
|
|
125
|
-
|
|
126
|
-
const p1 = session.submit([{ body: "a" }]);
|
|
127
|
-
const p2 = session.submit([{ body: "b" }]);
|
|
128
|
-
// suppress unhandled rejection warnings
|
|
129
|
-
p1.catch(() => {});
|
|
130
|
-
p2.catch(() => {});
|
|
131
|
-
|
|
132
|
-
await expect(p1).rejects.toBeTruthy();
|
|
133
|
-
await expect(p2).rejects.toBeTruthy();
|
|
134
|
-
|
|
135
|
-
// After error, queue should be empty; new submit should restart processing
|
|
136
|
-
appendSpy.mockResolvedValueOnce(makeAck(3));
|
|
137
|
-
const p3 = session.submit([{ body: "c" }]);
|
|
138
|
-
await expect(p3).resolves.toBeTruthy();
|
|
139
|
-
expect(appendSpy).toHaveBeenCalledTimes(2); // 1 throw + 1 success
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("updates lastSeenPosition after successful append", async () => {
|
|
143
|
-
const stream = makeStream();
|
|
144
|
-
vi.spyOn(stream, "append").mockResolvedValue(makeAck(42));
|
|
145
|
-
const session = await stream.appendSession();
|
|
146
|
-
await session.submit([{ body: "z" }]);
|
|
147
|
-
expect(session.lastSeenPosition?.end.seq_num).toBe(42);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { AppendAck } from "../generated";
|
|
3
|
-
import { S2Stream } from "../stream";
|
|
4
|
-
|
|
5
|
-
const fakeClient: any = {};
|
|
6
|
-
const makeStream = () => new S2Stream("test-stream", fakeClient);
|
|
7
|
-
const makeAck = (n: number): AppendAck => ({
|
|
8
|
-
start: { seq_num: n - 1, timestamp: 0 },
|
|
9
|
-
end: { seq_num: n, timestamp: 0 },
|
|
10
|
-
tail: { seq_num: n, timestamp: 0 },
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe("Batcher + AppendSession integration", () => {
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
vi.useFakeTimers();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
vi.useRealTimers();
|
|
20
|
-
vi.restoreAllMocks();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("linger-driven batching yields single session submission", async () => {
|
|
24
|
-
const stream = makeStream();
|
|
25
|
-
const session = await stream.appendSession();
|
|
26
|
-
const appendSpy = vi.spyOn(stream, "append").mockResolvedValue(makeAck(1));
|
|
27
|
-
const batcher = session.makeBatcher({
|
|
28
|
-
lingerDuration: 10,
|
|
29
|
-
maxBatchSize: 100,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const p1 = batcher.submit({ body: "a" });
|
|
33
|
-
const p2 = batcher.submit({ body: "b" });
|
|
34
|
-
|
|
35
|
-
await vi.advanceTimersByTimeAsync(12);
|
|
36
|
-
await Promise.all([p1, p2]);
|
|
37
|
-
|
|
38
|
-
expect(appendSpy).toHaveBeenCalledTimes(1);
|
|
39
|
-
expect(appendSpy.mock.calls?.[0]?.[0]).toHaveLength(2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("batch overflow increments match_seq_num across multiple flushes", async () => {
|
|
43
|
-
const stream = makeStream();
|
|
44
|
-
const session = await stream.appendSession();
|
|
45
|
-
const appendSpy = vi.spyOn(stream, "append");
|
|
46
|
-
appendSpy.mockResolvedValueOnce(makeAck(1));
|
|
47
|
-
appendSpy.mockResolvedValueOnce(makeAck(2));
|
|
48
|
-
const batcher = session.makeBatcher({
|
|
49
|
-
lingerDuration: 0,
|
|
50
|
-
maxBatchSize: 2,
|
|
51
|
-
match_seq_num: 5,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const p1 = batcher.submit([{ body: "1" }, { body: "2" }]);
|
|
55
|
-
batcher.flush();
|
|
56
|
-
await p1;
|
|
57
|
-
const p2 = batcher.submit([{ body: "3" }]);
|
|
58
|
-
batcher.flush();
|
|
59
|
-
await p2;
|
|
60
|
-
|
|
61
|
-
expect(appendSpy).toHaveBeenCalledTimes(2);
|
|
62
|
-
expect(appendSpy.mock.calls?.[0]?.[1]).toMatchObject({ match_seq_num: 5 });
|
|
63
|
-
expect(appendSpy.mock.calls?.[1]?.[1]).toMatchObject({ match_seq_num: 7 });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("batcher promise resolves with ack from session/stream", async () => {
|
|
67
|
-
const stream = makeStream();
|
|
68
|
-
const session = await stream.appendSession();
|
|
69
|
-
vi.spyOn(stream, "append").mockResolvedValue(makeAck(123));
|
|
70
|
-
const batcher = session.makeBatcher({
|
|
71
|
-
lingerDuration: 0,
|
|
72
|
-
maxBatchSize: 10,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const ackPromise = batcher.submit({ body: "x" });
|
|
76
|
-
batcher.flush();
|
|
77
|
-
const ack = await ackPromise;
|
|
78
|
-
expect(ack.end.seq_num).toBe(123);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { AppendAck } from "../generated";
|
|
3
|
-
import type { AppendRecord } from "../stream";
|
|
4
|
-
import { S2Stream } from "../stream";
|
|
5
|
-
|
|
6
|
-
const fakeClient: any = {};
|
|
7
|
-
const makeStream = () => new S2Stream("test-stream", fakeClient);
|
|
8
|
-
const makeAck = (n: number): AppendAck => ({
|
|
9
|
-
start: { seq_num: n - 1, timestamp: 0 },
|
|
10
|
-
end: { seq_num: n, timestamp: 0 },
|
|
11
|
-
tail: { seq_num: n, timestamp: 0 },
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe("Batcher", () => {
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.useFakeTimers();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
vi.useRealTimers();
|
|
21
|
-
vi.restoreAllMocks();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("batches submits within linger window into single session.submit", async () => {
|
|
25
|
-
const stream = makeStream();
|
|
26
|
-
const session = await stream.appendSession();
|
|
27
|
-
const submitSpy = vi.spyOn(session, "submit").mockResolvedValue(makeAck(1));
|
|
28
|
-
|
|
29
|
-
const batcher = session.makeBatcher({
|
|
30
|
-
lingerDuration: 20,
|
|
31
|
-
maxBatchSize: 10,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const p1 = batcher.submit({ body: "a" });
|
|
35
|
-
const p2 = batcher.submit({ body: "b" });
|
|
36
|
-
|
|
37
|
-
// nothing flushed yet
|
|
38
|
-
expect(submitSpy).toHaveBeenCalledTimes(0);
|
|
39
|
-
|
|
40
|
-
await vi.advanceTimersByTimeAsync(20);
|
|
41
|
-
|
|
42
|
-
// now one flush
|
|
43
|
-
await Promise.all([p1, p2]);
|
|
44
|
-
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
45
|
-
const firstCall = submitSpy.mock.calls[0] as unknown[];
|
|
46
|
-
const recordsArg = firstCall?.[0] as AppendRecord[];
|
|
47
|
-
expect(recordsArg).toHaveLength(2);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("manual flush cancels timer and sends immediately", async () => {
|
|
51
|
-
const stream = makeStream();
|
|
52
|
-
const session = await stream.appendSession();
|
|
53
|
-
const submitSpy = vi
|
|
54
|
-
.spyOn(session, "submit")
|
|
55
|
-
.mockResolvedValue(makeAck(99));
|
|
56
|
-
|
|
57
|
-
const batcher = session.makeBatcher({
|
|
58
|
-
lingerDuration: 1000,
|
|
59
|
-
maxBatchSize: 10,
|
|
60
|
-
});
|
|
61
|
-
const p = batcher.submit({ body: "x" });
|
|
62
|
-
batcher.flush();
|
|
63
|
-
|
|
64
|
-
await p;
|
|
65
|
-
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("close() flushes remaining and prevents further submit", async () => {
|
|
69
|
-
const stream = makeStream();
|
|
70
|
-
const session = await stream.appendSession();
|
|
71
|
-
vi.spyOn(session, "submit").mockResolvedValue(makeAck(1));
|
|
72
|
-
const batcher = session.makeBatcher({
|
|
73
|
-
lingerDuration: 1000,
|
|
74
|
-
maxBatchSize: 10,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const p = batcher.submit({ body: "x" });
|
|
78
|
-
await batcher.close();
|
|
79
|
-
await p;
|
|
80
|
-
|
|
81
|
-
await expect(batcher.submit({ body: "y" })).rejects.toMatchObject({
|
|
82
|
-
message: expect.stringContaining("Batcher is closed"),
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("abort rejects pending with S2Error, clears batch", async () => {
|
|
87
|
-
const stream = makeStream();
|
|
88
|
-
const session = await stream.appendSession();
|
|
89
|
-
vi.spyOn(session, "submit").mockResolvedValue(makeAck(1));
|
|
90
|
-
const batcher = session.makeBatcher({
|
|
91
|
-
lingerDuration: 1000,
|
|
92
|
-
maxBatchSize: 10,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const p = batcher.submit({ body: "x" });
|
|
96
|
-
const writer = batcher.getWriter?.() ?? batcher;
|
|
97
|
-
await writer.abort?.("stop");
|
|
98
|
-
|
|
99
|
-
await expect(p).rejects.toMatchObject({
|
|
100
|
-
message: expect.stringContaining("Batcher was aborted: stop"),
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("propagates fencing_token and auto-increments match_seq_num across batches", async () => {
|
|
105
|
-
const stream = makeStream();
|
|
106
|
-
const session = await stream.appendSession();
|
|
107
|
-
const submitSpy = vi.spyOn(session, "submit");
|
|
108
|
-
submitSpy.mockResolvedValueOnce(makeAck(1));
|
|
109
|
-
submitSpy.mockResolvedValueOnce(makeAck(2));
|
|
110
|
-
|
|
111
|
-
const batcher = session.makeBatcher({
|
|
112
|
-
lingerDuration: 0,
|
|
113
|
-
maxBatchSize: 2,
|
|
114
|
-
fencing_token: "ft",
|
|
115
|
-
match_seq_num: 10,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// First batch: two records
|
|
119
|
-
const p1 = batcher.submit([{ body: "a" }, { body: "b" }]);
|
|
120
|
-
batcher.flush();
|
|
121
|
-
await p1;
|
|
122
|
-
// Second batch: one record
|
|
123
|
-
const p2 = batcher.submit([{ body: "c" }]);
|
|
124
|
-
batcher.flush();
|
|
125
|
-
await p2;
|
|
126
|
-
|
|
127
|
-
expect(submitSpy).toHaveBeenCalledTimes(2);
|
|
128
|
-
const c0 = submitSpy.mock.calls[0] as unknown[];
|
|
129
|
-
const c1 = submitSpy.mock.calls[1] as unknown[];
|
|
130
|
-
expect(c0?.[1]).toMatchObject({
|
|
131
|
-
fencing_token: "ft",
|
|
132
|
-
match_seq_num: 10,
|
|
133
|
-
});
|
|
134
|
-
expect(c1?.[1]).toMatchObject({
|
|
135
|
-
fencing_token: "ft",
|
|
136
|
-
match_seq_num: 12,
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("array submit is atomic: if it won't fit, flush current then enqueue whole array", async () => {
|
|
141
|
-
const stream = makeStream();
|
|
142
|
-
const session = await stream.appendSession();
|
|
143
|
-
const submitSpy = vi.spyOn(session, "submit").mockResolvedValue(makeAck(1));
|
|
144
|
-
|
|
145
|
-
const batcher = session.makeBatcher({
|
|
146
|
-
lingerDuration: 0,
|
|
147
|
-
maxBatchSize: 3,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Fill current batch with two singles (no auto flush because linger=0)
|
|
151
|
-
batcher.submit({ body: "a" });
|
|
152
|
-
batcher.submit({ body: "b" });
|
|
153
|
-
|
|
154
|
-
// Now submit an array of 2 (atomic). Since 2+2>3, it flushes current 2,
|
|
155
|
-
// then enqueues both array records into the next batch
|
|
156
|
-
const p = batcher.submit([{ body: "c" }, { body: "d" }]);
|
|
157
|
-
|
|
158
|
-
// First flush happened immediately for the two singles
|
|
159
|
-
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
160
|
-
expect(
|
|
161
|
-
(submitSpy.mock.calls[0] as unknown[])?.[0] as AppendRecord[],
|
|
162
|
-
).toHaveLength(2);
|
|
163
|
-
|
|
164
|
-
// Explicitly flush the array batch because linger=0
|
|
165
|
-
batcher.flush();
|
|
166
|
-
await p;
|
|
167
|
-
expect(submitSpy).toHaveBeenCalledTimes(2);
|
|
168
|
-
expect(
|
|
169
|
-
(submitSpy.mock.calls[1] as unknown[])?.[0] as AppendRecord[],
|
|
170
|
-
).toHaveLength(2);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("array submit never splits across batches and resolves with single batch ack", async () => {
|
|
174
|
-
const stream = makeStream();
|
|
175
|
-
const session = await stream.appendSession();
|
|
176
|
-
const submitSpy = vi.spyOn(session, "submit");
|
|
177
|
-
|
|
178
|
-
// Two submits will occur: first flush of existing single; second flush of the array
|
|
179
|
-
submitSpy.mockResolvedValueOnce(makeAck(1));
|
|
180
|
-
submitSpy.mockResolvedValueOnce(makeAck(2));
|
|
181
|
-
const batcher = session.makeBatcher({ lingerDuration: 0, maxBatchSize: 2 });
|
|
182
|
-
|
|
183
|
-
// Fill current batch to 1
|
|
184
|
-
batcher.submit({ body: "x" });
|
|
185
|
-
// Submit 2-record array: should flush current (1), then submit both together (2)
|
|
186
|
-
const promise = batcher.submit([{ body: "a" }, { body: "b" }]);
|
|
187
|
-
|
|
188
|
-
expect(submitSpy).toHaveBeenCalledTimes(1);
|
|
189
|
-
expect(
|
|
190
|
-
(submitSpy.mock.calls[0] as unknown[])?.[0] as AppendRecord[],
|
|
191
|
-
).toHaveLength(1);
|
|
192
|
-
|
|
193
|
-
batcher.flush();
|
|
194
|
-
await promise;
|
|
195
|
-
expect(submitSpy).toHaveBeenCalledTimes(2);
|
|
196
|
-
expect(
|
|
197
|
-
(submitSpy.mock.calls[1] as unknown[])?.[0] as AppendRecord[],
|
|
198
|
-
).toHaveLength(2);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("array submit rejects if its single batch fails", async () => {
|
|
202
|
-
const stream = makeStream();
|
|
203
|
-
const session = await stream.appendSession();
|
|
204
|
-
const submitSpy = vi.spyOn(session, "submit");
|
|
205
|
-
|
|
206
|
-
submitSpy.mockRejectedValueOnce(new Error("batch failed"));
|
|
207
|
-
|
|
208
|
-
const batcher = session.makeBatcher({ lingerDuration: 0, maxBatchSize: 2 });
|
|
209
|
-
const promise = batcher.submit([{ body: "a" }, { body: "b" }]);
|
|
210
|
-
// suppress unhandled rejection warning
|
|
211
|
-
promise.catch(() => {});
|
|
212
|
-
|
|
213
|
-
batcher.flush();
|
|
214
|
-
await expect(promise).rejects.toThrow("batch failed");
|
|
215
|
-
});
|
|
216
|
-
});
|
package/src/tests/index.test.ts
DELETED
package/src/utils.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { AppendRecord as AppendRecordType } from "./stream";
|
|
2
|
-
|
|
3
|
-
type Headers =
|
|
4
|
-
| Record<string, string>
|
|
5
|
-
| Array<[string | Uint8Array, string | Uint8Array]>;
|
|
6
|
-
|
|
7
|
-
export type AppendRecord = AppendRecordType;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Helpers to construct appendable records.
|
|
11
|
-
*
|
|
12
|
-
* These helpers mirror the OpenAPI record schema and add convenience builders for S2 command records:
|
|
13
|
-
* - `make` creates a normal record
|
|
14
|
-
* - `command` creates a command record with an empty-name header set to the command name
|
|
15
|
-
* - `fence` is a command record enforcing a fencing token
|
|
16
|
-
* - `trim` is a command record that encodes a sequence number for trimming
|
|
17
|
-
*/
|
|
18
|
-
export const AppendRecord = {
|
|
19
|
-
make: (
|
|
20
|
-
body?: string | Uint8Array,
|
|
21
|
-
headers?: Headers,
|
|
22
|
-
timestamp?: AppendRecordType["timestamp"],
|
|
23
|
-
): AppendRecordType => {
|
|
24
|
-
return {
|
|
25
|
-
body,
|
|
26
|
-
headers,
|
|
27
|
-
timestamp,
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
command: (
|
|
31
|
-
/** Command name (e.g. "fence" or "trim"). */
|
|
32
|
-
command: string,
|
|
33
|
-
body?: string | Uint8Array,
|
|
34
|
-
additionalHeaders?: Headers,
|
|
35
|
-
timestamp?: AppendRecordType["timestamp"],
|
|
36
|
-
): AppendRecordType => {
|
|
37
|
-
const headers: AppendRecordType["headers"] = (() => {
|
|
38
|
-
if (!additionalHeaders) {
|
|
39
|
-
return [["", command]];
|
|
40
|
-
} else if (Array.isArray(additionalHeaders)) {
|
|
41
|
-
return [["", command] as [string, string], ...additionalHeaders];
|
|
42
|
-
} else {
|
|
43
|
-
return [
|
|
44
|
-
["", command] as [string, string],
|
|
45
|
-
...Object.entries(additionalHeaders).map(
|
|
46
|
-
([key, value]) => [key, value] as [string, string],
|
|
47
|
-
),
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
})();
|
|
51
|
-
return {
|
|
52
|
-
body,
|
|
53
|
-
headers,
|
|
54
|
-
timestamp,
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
fence: (
|
|
58
|
-
fencing_token: string,
|
|
59
|
-
additionalHeaders?: Headers,
|
|
60
|
-
timestamp?: AppendRecordType["timestamp"],
|
|
61
|
-
) => {
|
|
62
|
-
return AppendRecord.command(
|
|
63
|
-
"fence",
|
|
64
|
-
fencing_token,
|
|
65
|
-
additionalHeaders,
|
|
66
|
-
timestamp,
|
|
67
|
-
);
|
|
68
|
-
},
|
|
69
|
-
trim: (
|
|
70
|
-
seqNum: number | bigint,
|
|
71
|
-
additionalHeaders?: Headers,
|
|
72
|
-
timestamp?: AppendRecordType["timestamp"],
|
|
73
|
-
): AppendRecordType => {
|
|
74
|
-
// Encode sequence number as 8 big-endian bytes
|
|
75
|
-
const buffer = new Uint8Array(8);
|
|
76
|
-
const view = new DataView(buffer.buffer);
|
|
77
|
-
view.setBigUint64(0, BigInt(seqNum), false); // false = big-endian
|
|
78
|
-
return AppendRecord.command("trim", buffer, additionalHeaders, timestamp);
|
|
79
|
-
},
|
|
80
|
-
} as const;
|