@outfitter/contracts 0.1.0-rc.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.
@@ -0,0 +1,299 @@
1
+ import { TaggedErrorClass } from "better-result";
2
+ declare const ValidationErrorBase: TaggedErrorClass<"ValidationError", {
3
+ message: string;
4
+ field?: string;
5
+ }>;
6
+ declare const AssertionErrorBase: TaggedErrorClass<"AssertionError", {
7
+ message: string;
8
+ }>;
9
+ declare const NotFoundErrorBase: TaggedErrorClass<"NotFoundError", {
10
+ message: string;
11
+ resourceType: string;
12
+ resourceId: string;
13
+ }>;
14
+ declare const ConflictErrorBase: TaggedErrorClass<"ConflictError", {
15
+ message: string;
16
+ context?: Record<string, unknown>;
17
+ }>;
18
+ declare const PermissionErrorBase: TaggedErrorClass<"PermissionError", {
19
+ message: string;
20
+ context?: Record<string, unknown>;
21
+ }>;
22
+ declare const TimeoutErrorBase: TaggedErrorClass<"TimeoutError", {
23
+ message: string;
24
+ operation: string;
25
+ timeoutMs: number;
26
+ }>;
27
+ declare const RateLimitErrorBase: TaggedErrorClass<"RateLimitError", {
28
+ message: string;
29
+ retryAfterSeconds?: number;
30
+ }>;
31
+ declare const NetworkErrorBase: TaggedErrorClass<"NetworkError", {
32
+ message: string;
33
+ context?: Record<string, unknown>;
34
+ }>;
35
+ declare const InternalErrorBase: TaggedErrorClass<"InternalError", {
36
+ message: string;
37
+ context?: Record<string, unknown>;
38
+ }>;
39
+ declare const AuthErrorBase: TaggedErrorClass<"AuthError", {
40
+ message: string;
41
+ reason?: "missing" | "invalid" | "expired";
42
+ }>;
43
+ declare const CancelledErrorBase: TaggedErrorClass<"CancelledError", {
44
+ message: string;
45
+ }>;
46
+ /**
47
+ * Input validation failed.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * new ValidationError({ message: "Email format invalid", field: "email" });
52
+ * ```
53
+ */
54
+ declare class ValidationError extends ValidationErrorBase {
55
+ readonly category: "validation";
56
+ exitCode(): number;
57
+ statusCode(): number;
58
+ }
59
+ /**
60
+ * Assertion failed (invariant violation).
61
+ *
62
+ * Used by assertion utilities that return Result types instead of throwing.
63
+ * AssertionError indicates a programming bug — an invariant that should
64
+ * never be violated was broken. These are internal errors, not user input
65
+ * validation failures.
66
+ *
67
+ * **Category rationale**: Uses `internal` (not `validation`) because:
68
+ * - Assertions check **invariants** (programmer assumptions), not user input
69
+ * - A failed assertion means "this should be impossible if the code is correct"
70
+ * - User-facing validation uses {@link ValidationError} with helpful field info
71
+ * - HTTP 500 is correct: this is a server bug, not a client mistake
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // In domain logic after validation has passed
76
+ * const result = assertDefined(cachedValue, "Cache should always have value after init");
77
+ * if (result.isErr()) {
78
+ * return result; // Propagate as internal error
79
+ * }
80
+ * ```
81
+ *
82
+ * @see ValidationError - For user input validation failures (HTTP 400)
83
+ */
84
+ declare class AssertionError extends AssertionErrorBase {
85
+ readonly category: "internal";
86
+ exitCode(): number;
87
+ statusCode(): number;
88
+ }
89
+ /**
90
+ * Requested resource not found.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * new NotFoundError({ message: "note not found: abc123", resourceType: "note", resourceId: "abc123" });
95
+ * ```
96
+ */
97
+ declare class NotFoundError extends NotFoundErrorBase {
98
+ readonly category: "not_found";
99
+ exitCode(): number;
100
+ statusCode(): number;
101
+ }
102
+ /**
103
+ * State conflict (optimistic locking, concurrent modification).
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * new ConflictError({ message: "Resource was modified by another process" });
108
+ * ```
109
+ */
110
+ declare class ConflictError extends ConflictErrorBase {
111
+ readonly category: "conflict";
112
+ exitCode(): number;
113
+ statusCode(): number;
114
+ }
115
+ /**
116
+ * Authorization denied.
117
+ *
118
+ * @example
119
+ * ```typescript
120
+ * new PermissionError({ message: "Cannot delete read-only resource" });
121
+ * ```
122
+ */
123
+ declare class PermissionError extends PermissionErrorBase {
124
+ readonly category: "permission";
125
+ exitCode(): number;
126
+ statusCode(): number;
127
+ }
128
+ /**
129
+ * Operation timed out.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * new TimeoutError({ message: "Database query timed out after 5000ms", operation: "Database query", timeoutMs: 5000 });
134
+ * ```
135
+ */
136
+ declare class TimeoutError extends TimeoutErrorBase {
137
+ readonly category: "timeout";
138
+ exitCode(): number;
139
+ statusCode(): number;
140
+ }
141
+ /**
142
+ * Rate limit exceeded.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * new RateLimitError({ message: "Rate limit exceeded, retry after 60s", retryAfterSeconds: 60 });
147
+ * ```
148
+ */
149
+ declare class RateLimitError extends RateLimitErrorBase {
150
+ readonly category: "rate_limit";
151
+ exitCode(): number;
152
+ statusCode(): number;
153
+ }
154
+ /**
155
+ * Network/transport failure.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * new NetworkError({ message: "Connection refused to api.example.com" });
160
+ * ```
161
+ */
162
+ declare class NetworkError extends NetworkErrorBase {
163
+ readonly category: "network";
164
+ exitCode(): number;
165
+ statusCode(): number;
166
+ }
167
+ /**
168
+ * Unexpected internal error.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * new InternalError({ message: "Unexpected state in processor" });
173
+ * ```
174
+ */
175
+ declare class InternalError extends InternalErrorBase {
176
+ readonly category: "internal";
177
+ exitCode(): number;
178
+ statusCode(): number;
179
+ }
180
+ /**
181
+ * Authentication failed (missing or invalid credentials).
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * new AuthError({ message: "Invalid API key", reason: "invalid" });
186
+ * ```
187
+ */
188
+ declare class AuthError extends AuthErrorBase {
189
+ readonly category: "auth";
190
+ exitCode(): number;
191
+ statusCode(): number;
192
+ }
193
+ /**
194
+ * Operation cancelled by user or signal.
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * new CancelledError({ message: "Operation cancelled by user" });
199
+ * ```
200
+ */
201
+ declare class CancelledError extends CancelledErrorBase {
202
+ readonly category: "cancelled";
203
+ exitCode(): number;
204
+ statusCode(): number;
205
+ }
206
+ /**
207
+ * Union type of all concrete error class instances.
208
+ */
209
+ type AnyKitError = InstanceType<typeof ValidationError> | InstanceType<typeof AssertionError> | InstanceType<typeof NotFoundError> | InstanceType<typeof ConflictError> | InstanceType<typeof PermissionError> | InstanceType<typeof TimeoutError> | InstanceType<typeof RateLimitError> | InstanceType<typeof NetworkError> | InstanceType<typeof InternalError> | InstanceType<typeof AuthError> | InstanceType<typeof CancelledError>;
210
+ /**
211
+ * Type alias for backwards compatibility with handler signatures.
212
+ * Use AnyKitError for the union type.
213
+ */
214
+ type OutfitterError = AnyKitError;
215
+ import { Result } from "better-result";
216
+ /**
217
+ * Options for retry behavior.
218
+ */
219
+ interface RetryOptions {
220
+ /** Maximum number of retry attempts (default: 3) */
221
+ maxAttempts?: number;
222
+ /** Initial delay in milliseconds (default: 1000) */
223
+ initialDelayMs?: number;
224
+ /** Maximum delay in milliseconds (default: 30000) */
225
+ maxDelayMs?: number;
226
+ /** Exponential backoff multiplier (default: 2) */
227
+ backoffMultiplier?: number;
228
+ /** Whether to add jitter to delays (default: true) */
229
+ jitter?: boolean;
230
+ /** Predicate to determine if error is retryable */
231
+ isRetryable?: (error: OutfitterError) => boolean;
232
+ /** Abort signal for cancellation */
233
+ signal?: AbortSignal;
234
+ /** Callback invoked before each retry */
235
+ onRetry?: (attempt: number, error: OutfitterError, delayMs: number) => void;
236
+ }
237
+ /**
238
+ * Options for timeout behavior.
239
+ */
240
+ interface TimeoutOptions {
241
+ /** Timeout duration in milliseconds */
242
+ timeoutMs: number;
243
+ /** Operation name for error context */
244
+ operation?: string;
245
+ }
246
+ /**
247
+ * Retry an async operation with exponential backoff.
248
+ *
249
+ * Automatically retries transient errors (network, timeout, rate_limit)
250
+ * unless overridden with `isRetryable`.
251
+ *
252
+ * @typeParam T - Success type
253
+ * @param fn - Async function returning Result
254
+ * @param options - Retry configuration
255
+ * @returns Result from final attempt
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const result = await retry(
260
+ * () => fetchData(url),
261
+ * {
262
+ * maxAttempts: 5,
263
+ * initialDelayMs: 500,
264
+ * onRetry: (attempt, error) => {
265
+ * logger.warn(`Retry ${attempt}`, { error: error._tag });
266
+ * },
267
+ * }
268
+ * );
269
+ * ```
270
+ */
271
+ declare function retry<T>(fn: () => Promise<Result<T, OutfitterError>>, options?: RetryOptions): Promise<Result<T, OutfitterError>>;
272
+ /**
273
+ * Wrap an async operation with a timeout.
274
+ *
275
+ * Returns TimeoutError if operation doesn't complete within the specified duration.
276
+ *
277
+ * @typeParam T - Success type
278
+ * @typeParam E - Error type
279
+ * @param fn - Async function returning Result
280
+ * @param options - Timeout configuration
281
+ * @returns Result from operation or TimeoutError
282
+ *
283
+ * @example
284
+ * ```typescript
285
+ * const result = await withTimeout(
286
+ * () => slowOperation(),
287
+ * { timeoutMs: 5000, operation: "database query" }
288
+ * );
289
+ *
290
+ * if (result.isErr() && result.error._tag === "TimeoutError") {
291
+ * // Handle timeout
292
+ * }
293
+ * ```
294
+ */
295
+ declare function withTimeout<
296
+ T,
297
+ E extends OutfitterError
298
+ >(fn: () => Promise<Result<T, E>>, options: TimeoutOptions): Promise<Result<T, E | TimeoutError>>;
299
+ export { withTimeout, retry, TimeoutOptions, RetryOptions };
@@ -0,0 +1,82 @@
1
+ // @bun
2
+ import {
3
+ TimeoutError
4
+ } from "./errors.js";
5
+
6
+ // packages/contracts/src/resilience.ts
7
+ import { Result } from "better-result";
8
+ function defaultIsRetryable(error) {
9
+ return error.category === "network" || error.category === "timeout" || error.category === "rate_limit";
10
+ }
11
+ function calculateDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier, jitter) {
12
+ const baseDelay = initialDelayMs * backoffMultiplier ** (attempt - 1);
13
+ const cappedDelay = Math.min(baseDelay, maxDelayMs);
14
+ if (jitter) {
15
+ const jitterFactor = 0.5 + Math.random();
16
+ return Math.floor(cappedDelay * jitterFactor);
17
+ }
18
+ return cappedDelay;
19
+ }
20
+ function sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+ async function retry(fn, options) {
24
+ const maxAttempts = options?.maxAttempts ?? 3;
25
+ const initialDelayMs = options?.initialDelayMs ?? 1000;
26
+ const maxDelayMs = options?.maxDelayMs ?? 30000;
27
+ const backoffMultiplier = options?.backoffMultiplier ?? 2;
28
+ const jitter = options?.jitter ?? true;
29
+ const isRetryable = options?.isRetryable ?? defaultIsRetryable;
30
+ const onRetry = options?.onRetry;
31
+ const signal = options?.signal;
32
+ let lastError;
33
+ let attempt = 0;
34
+ while (attempt < maxAttempts) {
35
+ attempt++;
36
+ if (signal?.aborted) {
37
+ return Result.err(lastError ?? new TimeoutError({
38
+ message: "Operation cancelled",
39
+ operation: "retry",
40
+ timeoutMs: 0
41
+ }));
42
+ }
43
+ const result = await fn();
44
+ if (result.isOk()) {
45
+ return result;
46
+ }
47
+ lastError = result.error;
48
+ if (attempt >= maxAttempts || !isRetryable(lastError)) {
49
+ return result;
50
+ }
51
+ const delayMs = calculateDelay(attempt, initialDelayMs, maxDelayMs, backoffMultiplier, jitter);
52
+ if (onRetry) {
53
+ onRetry(attempt, lastError, delayMs);
54
+ }
55
+ await sleep(delayMs);
56
+ }
57
+ throw new Error("Unexpected: retry loop completed without returning a result");
58
+ }
59
+ async function withTimeout(fn, options) {
60
+ const { timeoutMs, operation = "operation" } = options;
61
+ let timeoutId;
62
+ const timeoutPromise = new Promise((resolve) => {
63
+ timeoutId = setTimeout(() => {
64
+ resolve(Result.err(new TimeoutError({
65
+ message: `${operation} timed out after ${timeoutMs}ms`,
66
+ operation,
67
+ timeoutMs
68
+ })));
69
+ }, timeoutMs);
70
+ });
71
+ try {
72
+ return await Promise.race([fn(), timeoutPromise]);
73
+ } finally {
74
+ if (timeoutId !== undefined) {
75
+ clearTimeout(timeoutId);
76
+ }
77
+ }
78
+ }
79
+ export {
80
+ withTimeout,
81
+ retry
82
+ };
@@ -0,0 +1,103 @@
1
+ import { Result } from "better-result";
2
+ /**
3
+ * Extract value from Ok, or compute default from error.
4
+ *
5
+ * Unlike `unwrapOr`, the default is computed lazily only on Err.
6
+ * This is useful when the default value is expensive to compute
7
+ * and should only be evaluated when needed.
8
+ *
9
+ * @param result - The Result to unwrap
10
+ * @param defaultFn - Function to compute default value from error
11
+ * @returns The success value or computed default
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const result = Result.err("not found");
16
+ * const value = unwrapOrElse(result, (error) => {
17
+ * console.log("Computing expensive default due to:", error);
18
+ * return expensiveComputation();
19
+ * });
20
+ * ```
21
+ */
22
+ declare const unwrapOrElse: <
23
+ T,
24
+ E
25
+ >(result: Result<T, E>, defaultFn: (error: E) => T) => T;
26
+ /**
27
+ * Return first Ok, or fallback if first is Err.
28
+ *
29
+ * Useful for trying alternative operations - if the first fails,
30
+ * fall back to an alternative Result.
31
+ *
32
+ * @param result - The primary Result to try
33
+ * @param fallback - The fallback Result if primary is Err
34
+ * @returns First Ok result, or fallback
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const primary = parseFromCache(key);
39
+ * const fallback = parseFromNetwork(key);
40
+ * const result = orElse(primary, fallback);
41
+ * ```
42
+ */
43
+ declare const orElse: <
44
+ T,
45
+ E,
46
+ F
47
+ >(result: Result<T, E>, fallback: Result<T, F>) => Result<T, F>;
48
+ /**
49
+ * Combine two Results into a tuple Result.
50
+ *
51
+ * Returns first error if either fails, evaluated left-to-right.
52
+ * Useful for combining independent operations that must all succeed.
53
+ *
54
+ * @param r1 - First Result
55
+ * @param r2 - Second Result
56
+ * @returns Result containing tuple of both values, or first error
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const user = fetchUser(id);
61
+ * const settings = fetchSettings(id);
62
+ * const combined = combine2(user, settings);
63
+ *
64
+ * if (combined.isOk()) {
65
+ * const [userData, userSettings] = combined.value;
66
+ * }
67
+ * ```
68
+ */
69
+ declare const combine2: <
70
+ T1,
71
+ T2,
72
+ E
73
+ >(r1: Result<T1, E>, r2: Result<T2, E>) => Result<[T1, T2], E>;
74
+ /**
75
+ * Combine three Results into a tuple Result.
76
+ *
77
+ * Returns first error if any fails, evaluated left-to-right.
78
+ * Useful for combining independent operations that must all succeed.
79
+ *
80
+ * @param r1 - First Result
81
+ * @param r2 - Second Result
82
+ * @param r3 - Third Result
83
+ * @returns Result containing tuple of all values, or first error
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const user = fetchUser(id);
88
+ * const settings = fetchSettings(id);
89
+ * const permissions = fetchPermissions(id);
90
+ * const combined = combine3(user, settings, permissions);
91
+ *
92
+ * if (combined.isOk()) {
93
+ * const [userData, userSettings, userPermissions] = combined.value;
94
+ * }
95
+ * ```
96
+ */
97
+ declare const combine3: <
98
+ T1,
99
+ T2,
100
+ T3,
101
+ E
102
+ >(r1: Result<T1, E>, r2: Result<T2, E>, r3: Result<T3, E>) => Result<[T1, T2, T3], E>;
103
+ export { unwrapOrElse, orElse, combine3, combine2 };
@@ -0,0 +1,13 @@
1
+ // @bun
2
+ import {
3
+ combine2,
4
+ combine3,
5
+ orElse,
6
+ unwrapOrElse
7
+ } from "./utilities.js";
8
+ export {
9
+ unwrapOrElse,
10
+ orElse,
11
+ combine3,
12
+ combine2
13
+ };
@@ -0,0 +1,103 @@
1
+ import { Result } from "better-result";
2
+ /**
3
+ * Extract value from Ok, or compute default from error.
4
+ *
5
+ * Unlike `unwrapOr`, the default is computed lazily only on Err.
6
+ * This is useful when the default value is expensive to compute
7
+ * and should only be evaluated when needed.
8
+ *
9
+ * @param result - The Result to unwrap
10
+ * @param defaultFn - Function to compute default value from error
11
+ * @returns The success value or computed default
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const result = Result.err("not found");
16
+ * const value = unwrapOrElse(result, (error) => {
17
+ * console.log("Computing expensive default due to:", error);
18
+ * return expensiveComputation();
19
+ * });
20
+ * ```
21
+ */
22
+ declare const unwrapOrElse: <
23
+ T,
24
+ E
25
+ >(result: Result<T, E>, defaultFn: (error: E) => T) => T;
26
+ /**
27
+ * Return first Ok, or fallback if first is Err.
28
+ *
29
+ * Useful for trying alternative operations - if the first fails,
30
+ * fall back to an alternative Result.
31
+ *
32
+ * @param result - The primary Result to try
33
+ * @param fallback - The fallback Result if primary is Err
34
+ * @returns First Ok result, or fallback
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const primary = parseFromCache(key);
39
+ * const fallback = parseFromNetwork(key);
40
+ * const result = orElse(primary, fallback);
41
+ * ```
42
+ */
43
+ declare const orElse: <
44
+ T,
45
+ E,
46
+ F
47
+ >(result: Result<T, E>, fallback: Result<T, F>) => Result<T, F>;
48
+ /**
49
+ * Combine two Results into a tuple Result.
50
+ *
51
+ * Returns first error if either fails, evaluated left-to-right.
52
+ * Useful for combining independent operations that must all succeed.
53
+ *
54
+ * @param r1 - First Result
55
+ * @param r2 - Second Result
56
+ * @returns Result containing tuple of both values, or first error
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const user = fetchUser(id);
61
+ * const settings = fetchSettings(id);
62
+ * const combined = combine2(user, settings);
63
+ *
64
+ * if (combined.isOk()) {
65
+ * const [userData, userSettings] = combined.value;
66
+ * }
67
+ * ```
68
+ */
69
+ declare const combine2: <
70
+ T1,
71
+ T2,
72
+ E
73
+ >(r1: Result<T1, E>, r2: Result<T2, E>) => Result<[T1, T2], E>;
74
+ /**
75
+ * Combine three Results into a tuple Result.
76
+ *
77
+ * Returns first error if any fails, evaluated left-to-right.
78
+ * Useful for combining independent operations that must all succeed.
79
+ *
80
+ * @param r1 - First Result
81
+ * @param r2 - Second Result
82
+ * @param r3 - Third Result
83
+ * @returns Result containing tuple of all values, or first error
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const user = fetchUser(id);
88
+ * const settings = fetchSettings(id);
89
+ * const permissions = fetchPermissions(id);
90
+ * const combined = combine3(user, settings, permissions);
91
+ *
92
+ * if (combined.isOk()) {
93
+ * const [userData, userSettings, userPermissions] = combined.value;
94
+ * }
95
+ * ```
96
+ */
97
+ declare const combine3: <
98
+ T1,
99
+ T2,
100
+ T3,
101
+ E
102
+ >(r1: Result<T1, E>, r2: Result<T2, E>, r3: Result<T3, E>) => Result<[T1, T2, T3], E>;
103
+ export { unwrapOrElse, orElse, combine3, combine2 };
@@ -0,0 +1,31 @@
1
+ // @bun
2
+ // packages/contracts/src/result/utilities.ts
3
+ import { Result } from "better-result";
4
+ var unwrapOrElse = (result, defaultFn) => {
5
+ return result.isOk() ? result.value : defaultFn(result.error);
6
+ };
7
+ var orElse = (result, fallback) => {
8
+ return result.isOk() ? result : fallback;
9
+ };
10
+ var combine2 = (r1, r2) => {
11
+ if (r1.isErr())
12
+ return r1;
13
+ if (r2.isErr())
14
+ return r2;
15
+ return Result.ok([r1.value, r2.value]);
16
+ };
17
+ var combine3 = (r1, r2, r3) => {
18
+ if (r1.isErr())
19
+ return r1;
20
+ if (r2.isErr())
21
+ return r2;
22
+ if (r3.isErr())
23
+ return r3;
24
+ return Result.ok([r1.value, r2.value, r3.value]);
25
+ };
26
+ export {
27
+ unwrapOrElse,
28
+ orElse,
29
+ combine3,
30
+ combine2
31
+ };