Skip to content

Commit 717e65e

Browse files
committed
custom error types - closes #9
1 parent 487352b commit 717e65e

File tree

5 files changed

+134
-43
lines changed

5 files changed

+134
-43
lines changed

.changeset/modern-regions-read.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@s2-dev/streamstore": patch
3+
---
4+
5+
custom error types

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3232

3333
# Finder (MacOS) folder config
3434
.DS_Store
35+
36+
.claude

src/error.ts

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,115 @@
88
export class S2Error extends Error {
99
public readonly code?: string;
1010
public readonly status?: number;
11-
public readonly data?: Record<string, unknown>;
1211

1312
constructor({
1413
message,
1514
code,
1615
status,
17-
data,
1816
}: {
19-
/** Human-readable error message. */
2017
message: string;
2118
code?: string;
2219
status?: number;
23-
/** Additional error details when available. */
24-
data?: Record<string, unknown>;
2520
}) {
26-
// Include full data in the error message for better visibility
27-
const dataStr = data ? `\nData: ${JSON.stringify(data, null, 2)}` : "";
28-
super(`${message}${dataStr}`);
21+
super(message);
2922
this.code = code;
3023
this.status = status;
31-
this.data = data;
3224
this.name = "S2Error";
3325
}
26+
}
27+
28+
/**
29+
* Thrown when an append operation fails due to a sequence number mismatch.
30+
*
31+
* This occurs when you specify a `matchSeqNum` condition in your append request,
32+
* but the current tail sequence number of the stream doesn't match.
33+
*
34+
* The `expectedSeqNum` property contains the actual next sequence number
35+
* that should be used for a successful append.
36+
*/
37+
export class SeqNumMismatchError extends S2Error {
38+
/** The expected next sequence number for the stream. */
39+
public readonly expectedSeqNum: number;
3440

35-
public toString() {
36-
return `${this.message} (code: ${this.code}, status: ${this.status}, data: ${JSON.stringify(this.data, null, 2)})`;
41+
constructor({
42+
message,
43+
code,
44+
status,
45+
expectedSeqNum,
46+
}: {
47+
message: string;
48+
code?: string;
49+
status?: number;
50+
expectedSeqNum: number;
51+
}) {
52+
super({
53+
message: `${message}\nExpected sequence number: ${expectedSeqNum}`,
54+
code,
55+
status,
56+
});
57+
this.name = "SeqNumMismatchError";
58+
this.expectedSeqNum = expectedSeqNum;
3759
}
60+
}
61+
62+
/**
63+
* Thrown when an append operation fails due to a fencing token mismatch.
64+
*
65+
* This occurs when you specify a `fencingToken` condition in your append request,
66+
* but the current fencing token of the stream doesn't match.
67+
*
68+
* The `expectedFencingToken` property contains the actual fencing token
69+
* that should be used for a successful append.
70+
*/
71+
export class FencingTokenMismatchError extends S2Error {
72+
/** The expected fencing token for the stream. */
73+
public readonly expectedFencingToken: string;
3874

39-
public toJSON() {
40-
return {
41-
message: this.message,
42-
code: this.code,
43-
status: this.status,
44-
data: this.data,
45-
};
75+
constructor({
76+
message,
77+
code,
78+
status,
79+
expectedFencingToken,
80+
}: {
81+
message: string;
82+
code?: string;
83+
status?: number;
84+
expectedFencingToken: string;
85+
}) {
86+
super({
87+
message: `${message}\nExpected fencing token: ${expectedFencingToken}`,
88+
code,
89+
status,
90+
});
91+
this.name = "FencingTokenMismatchError";
92+
this.expectedFencingToken = expectedFencingToken;
4693
}
94+
}
4795

48-
public [Symbol.for("nodejs.util.inspect.custom")]() {
49-
return {
50-
name: "S2Error",
51-
message: this.message,
52-
code: this.code,
53-
status: this.status,
54-
data: this.data,
55-
stack: this.stack,
56-
};
96+
/**
97+
* Thrown when a read operation fails because the requested position is beyond the stream tail.
98+
*
99+
* This occurs when you specify a `startSeqNum` that is greater than the current tail
100+
* of the stream (HTTP 416 Range Not Satisfiable).
101+
*
102+
* To handle this gracefully, you can set `clamp: true` in your read options to
103+
* automatically start from the tail instead of throwing an error.
104+
*/
105+
export class RangeNotSatisfiableError extends S2Error {
106+
constructor({
107+
message = "Range not satisfiable: requested position is beyond the stream tail. Use 'clamp: true' to start from the tail instead.",
108+
code,
109+
status = 416,
110+
}: {
111+
message?: string;
112+
code?: string;
113+
status?: number;
114+
} = {}) {
115+
super({
116+
message,
117+
code,
118+
status,
119+
});
120+
this.name = "RangeNotSatisfiableError";
57121
}
58122
}

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export type {
1010
ListBasinsArgs,
1111
ReconfigureBasinArgs,
1212
} from "./basins.js";
13-
export { S2Error } from "./error.js";
13+
export {
14+
FencingTokenMismatchError,
15+
RangeNotSatisfiableError,
16+
S2Error,
17+
SeqNumMismatchError,
18+
} from "./error.js";
1419
export type {
1520
AccessTokenInfo,
1621
AccessTokenScope,

src/stream.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { S2RequestOptions } from "./common.js";
2-
import { S2Error } from "./error.js";
2+
import {
3+
FencingTokenMismatchError,
4+
RangeNotSatisfiableError,
5+
S2Error,
6+
SeqNumMismatchError,
7+
} from "./error.js";
38
import type { Client } from "./generated/client/types.gen.js";
49
import {
510
type AppendAck,
@@ -84,11 +89,8 @@ export class S2Stream {
8489
});
8590
} else {
8691
// special case for 416 - Range Not Satisfiable
87-
throw new S2Error({
88-
message:
89-
"Range not satisfiable: requested position is beyond the stream tail. Use 'clamp: true' to start from the tail instead.",
92+
throw new RangeNotSatisfiableError({
9093
status: response.response.status,
91-
data: response.error,
9294
});
9395
}
9496
}
@@ -199,12 +201,28 @@ export class S2Stream {
199201
status: response.response.status,
200202
});
201203
} else {
202-
// special case for 412
203-
throw new S2Error({
204-
message: "Append condition failed",
205-
status: response.response.status,
206-
data: response.error,
207-
});
204+
// special case for 412 - append condition failed
205+
if ("seq_num_mismatch" in response.error) {
206+
throw new SeqNumMismatchError({
207+
message: "Append condition failed: sequence number mismatch",
208+
code: "APPEND_CONDITION_FAILED",
209+
status: response.response.status,
210+
expectedSeqNum: response.error.seq_num_mismatch,
211+
});
212+
} else if ("fencing_token_mismatch" in response.error) {
213+
throw new FencingTokenMismatchError({
214+
message: "Append condition failed: fencing token mismatch",
215+
code: "APPEND_CONDITION_FAILED",
216+
status: response.response.status,
217+
expectedFencingToken: response.error.fencing_token_mismatch,
218+
});
219+
} else {
220+
// fallback for unknown 412 error format
221+
throw new S2Error({
222+
message: "Append condition failed",
223+
status: response.response.status,
224+
});
225+
}
208226
}
209227
}
210228
return response.data;
@@ -297,11 +315,8 @@ class ReadSession<
297315
});
298316
} else {
299317
// special case for 416 - Range Not Satisfiable
300-
throw new S2Error({
301-
message:
302-
"Range not satisfiable: requested position is beyond the stream tail. Use 'clamp: true' to start from the tail instead.",
318+
throw new RangeNotSatisfiableError({
303319
status: response.response.status,
304-
data: response.error,
305320
});
306321
}
307322
}

0 commit comments

Comments
 (0)