splitwise
Version:
A TypeScript SDK for the Splitwise API.
173 lines • 7.24 kB
JavaScript
;
/**
* Typed error hierarchy for the Splitwise SDK, following the Stripe pattern.
*
* SplitwiseError (base)
* ├── SplitwiseApiError (HTTP errors from the API)
* │ ├── SplitwiseAuthenticationError (401)
* │ ├── SplitwiseForbiddenError (403)
* │ ├── SplitwiseNotFoundError (404)
* │ ├── SplitwiseValidationError (400)
* │ ├── SplitwiseRateLimitError (429)
* │ ├── SplitwiseServerError (5xx)
* │ └── SplitwiseConstraintError (200 with success:false / non-empty errors)
* └── SplitwiseConnectionError (network failures)
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SplitwiseConnectionError = exports.SplitwiseConstraintError = exports.SplitwiseServerError = exports.SplitwiseRateLimitError = exports.SplitwiseValidationError = exports.SplitwiseNotFoundError = exports.SplitwiseForbiddenError = exports.SplitwiseAuthenticationError = exports.SplitwiseApiError = exports.SplitwiseError = void 0;
exports.parseRetryAfter = parseRetryAfter;
exports.createApiError = createApiError;
class SplitwiseError extends Error {
constructor(message) {
super(message);
this.name = 'SplitwiseError';
}
}
exports.SplitwiseError = SplitwiseError;
class SplitwiseApiError extends SplitwiseError {
statusCode;
code;
raw;
constructor(statusCode, message, code, raw) {
super(message);
this.name = 'SplitwiseApiError';
this.statusCode = statusCode;
this.code = code;
this.raw = raw;
}
}
exports.SplitwiseApiError = SplitwiseApiError;
class SplitwiseAuthenticationError extends SplitwiseApiError {
constructor(message, code, raw) {
super(401, message, code, raw);
this.name = 'SplitwiseAuthenticationError';
}
}
exports.SplitwiseAuthenticationError = SplitwiseAuthenticationError;
class SplitwiseForbiddenError extends SplitwiseApiError {
constructor(message, code, raw) {
super(403, message, code, raw);
this.name = 'SplitwiseForbiddenError';
}
}
exports.SplitwiseForbiddenError = SplitwiseForbiddenError;
class SplitwiseNotFoundError extends SplitwiseApiError {
constructor(message, code, raw) {
super(404, message, code, raw);
this.name = 'SplitwiseNotFoundError';
}
}
exports.SplitwiseNotFoundError = SplitwiseNotFoundError;
class SplitwiseValidationError extends SplitwiseApiError {
constructor(message, code, raw) {
super(400, message, code, raw);
this.name = 'SplitwiseValidationError';
}
}
exports.SplitwiseValidationError = SplitwiseValidationError;
class SplitwiseRateLimitError extends SplitwiseApiError {
/**
* Server-suggested wait time in seconds, parsed from the Retry-After
* header. Handles both delta-seconds (e.g. "120") and HTTP-date formats.
* Undefined when the server didn't send the header or it was malformed.
*/
retryAfter;
constructor(message, code, raw, retryAfter) {
super(429, message, code, raw);
this.name = 'SplitwiseRateLimitError';
this.retryAfter = retryAfter;
}
}
exports.SplitwiseRateLimitError = SplitwiseRateLimitError;
class SplitwiseServerError extends SplitwiseApiError {
constructor(statusCode, message, code, raw) {
super(statusCode, message, code, raw);
this.name = 'SplitwiseServerError';
}
}
exports.SplitwiseServerError = SplitwiseServerError;
/**
* Splitwise's "destructive" endpoints (delete_*, undelete_*, add_user_to_group,
* remove_user_from_group) and some create/update endpoints can return HTTP 200
* with `success: false` or a non-empty `errors` field when the operation
* couldn't complete for a domain reason (e.g. trying to delete a friend with a
* non-zero balance). The SDK surfaces these as a typed exception following the
* Stripe model -- failures are always thrown, never returned as data.
*/
class SplitwiseConstraintError extends SplitwiseApiError {
constructor(message, code, raw) {
super(200, message, code, raw);
this.name = 'SplitwiseConstraintError';
}
}
exports.SplitwiseConstraintError = SplitwiseConstraintError;
class SplitwiseConnectionError extends SplitwiseError {
cause;
constructor(message, cause) {
super(message);
this.name = 'SplitwiseConnectionError';
this.cause = cause;
}
}
exports.SplitwiseConnectionError = SplitwiseConnectionError;
/**
* Parse a Retry-After header value, returning the delay in seconds.
*
* Per RFC 7231 § 7.1.3 the value can be either:
* - a non-negative integer (delta-seconds), e.g. "120"
* - an HTTP-date, e.g. "Wed, 21 Oct 2026 07:28:00 GMT"
*
* Returns undefined if the value is missing, malformed, or in the past.
*/
function parseRetryAfter(raw, now = () => Date.now()) {
if (raw === null || raw === undefined || raw.length === 0)
return undefined;
const trimmed = raw.trim();
// Try delta-seconds first. Use a regex to reject mixed input like "12abc"
// that Number() would otherwise coerce to NaN-but-then-misleading.
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
const seconds = Number(trimmed);
return seconds >= 0 ? seconds : undefined;
}
// Fall back to HTTP-date. Date.parse() is too permissive (e.g. it accepts
// bare strings like "-5" as years), so we require the input to look like a
// proper HTTP-date. RFC 7231 allows three formats:
// IMF-fixdate : "Sun, 06 Nov 1994 08:49:37 GMT" (weekday + comma)
// obsolete RFC 850: "Sunday, 06-Nov-94 08:49:37 GMT" (weekday + comma)
// ANSI C asctime : "Sun Nov 6 08:49:37 1994" (weekday + space + month name)
// All three start with a weekday name; the next non-space char is either
// a comma or another letter (the month name in asctime).
if (!/^[A-Za-z]+(?:,|\s+[A-Za-z])/.test(trimmed))
return undefined;
const epochMs = Date.parse(trimmed);
if (Number.isNaN(epochMs))
return undefined;
const deltaSeconds = Math.max(0, Math.ceil((epochMs - now()) / 1000));
return deltaSeconds;
}
/**
* Maps an HTTP status code to the appropriate SplitwiseApiError subclass.
*/
function createApiError(statusCode, message, code, raw, headers) {
switch (statusCode) {
case 400:
return new SplitwiseValidationError(message, code, raw);
case 401:
return new SplitwiseAuthenticationError(message, code, raw);
case 403:
return new SplitwiseForbiddenError(message, code, raw);
case 404:
return new SplitwiseNotFoundError(message, code, raw);
case 429: {
const retryAfterHeader = headers?.get('retry-after');
const retryAfter = parseRetryAfter(retryAfterHeader, () => Date.now());
return new SplitwiseRateLimitError(message, code, raw, retryAfter);
}
default:
if (statusCode >= 500 && statusCode < 600) {
return new SplitwiseServerError(statusCode, message, code, raw);
}
return new SplitwiseApiError(statusCode, message, code, raw);
}
}
//# sourceMappingURL=errors.js.map