@s2-dev/streamstore 0.16.0 → 0.16.2

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 (158) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1 -2
  3. package/dist/accessTokens.d.ts +37 -0
  4. package/dist/accessTokens.d.ts.map +1 -0
  5. package/dist/accessTokens.js +74 -0
  6. package/dist/accessTokens.js.map +1 -0
  7. package/dist/basin.d.ts +26 -0
  8. package/dist/basin.d.ts.map +1 -0
  9. package/dist/basin.js +34 -0
  10. package/dist/basin.js.map +1 -0
  11. package/dist/basins.d.ts +53 -0
  12. package/dist/basins.d.ts.map +1 -0
  13. package/dist/basins.js +115 -0
  14. package/dist/basins.js.map +1 -0
  15. package/dist/common.d.ts +44 -0
  16. package/dist/common.d.ts.map +1 -0
  17. package/dist/common.js +2 -0
  18. package/dist/common.js.map +1 -0
  19. package/dist/error.d.ts +28 -0
  20. package/dist/error.d.ts.map +1 -0
  21. package/dist/error.js +43 -0
  22. package/dist/error.js.map +1 -0
  23. package/dist/generated/client/client.gen.d.ts +3 -0
  24. package/dist/generated/client/client.gen.d.ts.map +1 -0
  25. package/dist/generated/client/client.gen.js +205 -0
  26. package/dist/generated/client/client.gen.js.map +1 -0
  27. package/dist/generated/client/index.d.ts +9 -0
  28. package/dist/generated/client/index.d.ts.map +1 -0
  29. package/dist/generated/client/index.js +7 -0
  30. package/dist/generated/client/index.js.map +1 -0
  31. package/dist/generated/client/types.gen.d.ts +125 -0
  32. package/dist/generated/client/types.gen.d.ts.map +1 -0
  33. package/dist/generated/client/types.gen.js +3 -0
  34. package/dist/generated/client/types.gen.js.map +1 -0
  35. package/dist/generated/client/utils.gen.d.ts +34 -0
  36. package/dist/generated/client/utils.gen.d.ts.map +1 -0
  37. package/dist/generated/client/utils.gen.js +231 -0
  38. package/dist/generated/client/utils.gen.js.map +1 -0
  39. package/{src/generated/client.gen.ts → dist/generated/client.gen.d.ts} +3 -8
  40. package/dist/generated/client.gen.d.ts.map +1 -0
  41. package/dist/generated/client.gen.js +6 -0
  42. package/dist/generated/client.gen.js.map +1 -0
  43. package/dist/generated/core/auth.gen.d.ts +19 -0
  44. package/dist/generated/core/auth.gen.d.ts.map +1 -0
  45. package/dist/generated/core/auth.gen.js +15 -0
  46. package/dist/generated/core/auth.gen.js.map +1 -0
  47. package/dist/generated/core/bodySerializer.gen.d.ts +18 -0
  48. package/dist/generated/core/bodySerializer.gen.d.ts.map +1 -0
  49. package/dist/generated/core/bodySerializer.gen.js +58 -0
  50. package/dist/generated/core/bodySerializer.gen.js.map +1 -0
  51. package/dist/generated/core/params.gen.d.ts +34 -0
  52. package/dist/generated/core/params.gen.d.ts.map +1 -0
  53. package/dist/generated/core/params.gen.js +89 -0
  54. package/dist/generated/core/params.gen.js.map +1 -0
  55. package/dist/generated/core/pathSerializer.gen.d.ts +34 -0
  56. package/dist/generated/core/pathSerializer.gen.d.ts.map +1 -0
  57. package/dist/generated/core/pathSerializer.gen.js +115 -0
  58. package/dist/generated/core/pathSerializer.gen.js.map +1 -0
  59. package/dist/generated/core/queryKeySerializer.gen.d.ts +19 -0
  60. package/dist/generated/core/queryKeySerializer.gen.d.ts.map +1 -0
  61. package/dist/generated/core/queryKeySerializer.gen.js +100 -0
  62. package/dist/generated/core/queryKeySerializer.gen.js.map +1 -0
  63. package/dist/generated/core/serverSentEvents.gen.d.ts +72 -0
  64. package/dist/generated/core/serverSentEvents.gen.d.ts.map +1 -0
  65. package/dist/generated/core/serverSentEvents.gen.js +136 -0
  66. package/dist/generated/core/serverSentEvents.gen.js.map +1 -0
  67. package/dist/generated/core/types.gen.d.ts +79 -0
  68. package/dist/generated/core/types.gen.d.ts.map +1 -0
  69. package/dist/generated/core/types.gen.js +3 -0
  70. package/dist/generated/core/types.gen.js.map +1 -0
  71. package/dist/generated/core/utils.gen.d.ts +20 -0
  72. package/dist/generated/core/utils.gen.d.ts.map +1 -0
  73. package/dist/generated/core/utils.gen.js +88 -0
  74. package/dist/generated/core/utils.gen.js.map +1 -0
  75. package/dist/generated/index.d.ts +3 -0
  76. package/dist/generated/index.d.ts.map +1 -0
  77. package/{src/generated/index.ts → dist/generated/index.js} +1 -2
  78. package/dist/generated/index.js.map +1 -0
  79. package/dist/generated/sdk.gen.d.ts +100 -0
  80. package/dist/generated/sdk.gen.d.ts.map +1 -0
  81. package/dist/generated/sdk.gen.js +350 -0
  82. package/dist/generated/sdk.gen.js.map +1 -0
  83. package/{src/generated/types.gen.ts → dist/generated/types.gen.d.ts} +1 -158
  84. package/dist/generated/types.gen.d.ts.map +1 -0
  85. package/dist/generated/types.gen.js +3 -0
  86. package/dist/generated/types.gen.js.map +1 -0
  87. package/dist/index.d.ts +10 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +4 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/lib/event-stream.d.ts +26 -0
  92. package/dist/lib/event-stream.d.ts.map +1 -0
  93. package/dist/lib/event-stream.js +138 -0
  94. package/dist/lib/event-stream.js.map +1 -0
  95. package/dist/metrics.d.ts +44 -0
  96. package/dist/metrics.d.ts.map +1 -0
  97. package/dist/metrics.js +83 -0
  98. package/dist/metrics.js.map +1 -0
  99. package/dist/s2.d.ts +38 -0
  100. package/dist/s2.d.ts.map +1 -0
  101. package/dist/s2.js +56 -0
  102. package/dist/s2.js.map +1 -0
  103. package/dist/stream.d.ts +156 -0
  104. package/dist/stream.d.ts.map +1 -0
  105. package/dist/stream.js +598 -0
  106. package/dist/stream.js.map +1 -0
  107. package/dist/streams.d.ts +52 -0
  108. package/dist/streams.d.ts.map +1 -0
  109. package/dist/streams.js +114 -0
  110. package/dist/streams.js.map +1 -0
  111. package/dist/utils.d.ts +20 -0
  112. package/dist/utils.d.ts.map +1 -0
  113. package/dist/utils.js +52 -0
  114. package/dist/utils.js.map +1 -0
  115. package/package.json +13 -4
  116. package/.changeset/README.md +0 -8
  117. package/.changeset/config.json +0 -11
  118. package/.claude/settings.local.json +0 -9
  119. package/.github/workflows/ci.yml +0 -59
  120. package/.github/workflows/publish.yml +0 -35
  121. package/CHANGELOG.md +0 -7
  122. package/biome.json +0 -30
  123. package/bun.lock +0 -598
  124. package/examples/append.ts +0 -84
  125. package/examples/kitchen-sink.ts +0 -73
  126. package/examples/read.ts +0 -30
  127. package/openapi-ts.config.ts +0 -7
  128. package/src/accessTokens.ts +0 -100
  129. package/src/basin.ts +0 -43
  130. package/src/basins.ts +0 -154
  131. package/src/common.ts +0 -45
  132. package/src/error.ts +0 -58
  133. package/src/generated/client/client.gen.ts +0 -268
  134. package/src/generated/client/index.ts +0 -26
  135. package/src/generated/client/types.gen.ts +0 -268
  136. package/src/generated/client/utils.gen.ts +0 -331
  137. package/src/generated/core/auth.gen.ts +0 -42
  138. package/src/generated/core/bodySerializer.gen.ts +0 -92
  139. package/src/generated/core/params.gen.ts +0 -153
  140. package/src/generated/core/pathSerializer.gen.ts +0 -181
  141. package/src/generated/core/queryKeySerializer.gen.ts +0 -136
  142. package/src/generated/core/serverSentEvents.gen.ts +0 -264
  143. package/src/generated/core/types.gen.ts +0 -118
  144. package/src/generated/core/utils.gen.ts +0 -143
  145. package/src/generated/sdk.gen.ts +0 -387
  146. package/src/index.ts +0 -66
  147. package/src/lib/event-stream.ts +0 -167
  148. package/src/metrics.ts +0 -106
  149. package/src/s2.ts +0 -65
  150. package/src/stream.ts +0 -791
  151. package/src/streams.ts +0 -156
  152. package/src/tests/appendSession.test.ts +0 -149
  153. package/src/tests/batcher-session.integration.test.ts +0 -80
  154. package/src/tests/batcher.test.ts +0 -216
  155. package/src/tests/index.test.ts +0 -7
  156. package/src/utils.ts +0 -80
  157. package/tsconfig.build.json +0 -10
  158. 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
- });
@@ -1,7 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- describe("S2", () => {
4
- it("should succeed", () => {
5
- expect(true).toBe(true);
6
- });
7
- });
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;
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "include": ["src/**/*"],
4
- "exclude": [
5
- "src/**/*.test.ts",
6
- "**/*.test.ts",
7
- "examples",
8
- "openapi-ts.config.ts"
9
- ]
10
- }