@cometloop/safe 0.0.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,378 @@
1
+ type Falsy = false | 0 | '' | null | undefined | 0n | void;
2
+ /**
3
+ * Strips falsy members from E via distributive conditional.
4
+ * When E is purely falsy (e.g. null, false, 0), the result is `never`,
5
+ * making the parseError return type unsatisfiable — a compile error.
6
+ *
7
+ * For union types like `string | null`, the falsy member (`null`) is
8
+ * stripped, so `parseError` must return `string` — which means
9
+ * `(e: unknown) => e?.message ?? null` correctly fails to compile.
10
+ */
11
+ type NonFalsy<E> = E extends Falsy ? never : E;
12
+ type SafeOk<T> = readonly [T, null] & {
13
+ readonly ok: true;
14
+ readonly value: T;
15
+ readonly error: null;
16
+ };
17
+ type SafeErr<E> = readonly [null, E] & {
18
+ readonly ok: false;
19
+ readonly value: null;
20
+ readonly error: E;
21
+ };
22
+ type SafeResult<T, E = Error> = SafeOk<T> | SafeErr<E>;
23
+ type SafeOkObj<T> = {
24
+ readonly ok: true;
25
+ readonly data: T;
26
+ readonly error: null;
27
+ };
28
+ type SafeErrObj<E> = {
29
+ readonly ok: false;
30
+ readonly data: null;
31
+ readonly error: E;
32
+ };
33
+ type SafeResultObj<T, E = Error> = SafeOkObj<T> | SafeErrObj<E>;
34
+ declare function ok<T>(value: T): SafeOk<T>;
35
+ declare function err<E>(error: E): SafeErr<E>;
36
+ declare function okObj<T>(data: T): SafeOkObj<T>;
37
+ declare function errObj<E>(error: E): SafeErrObj<E>;
38
+ declare class TimeoutError extends Error {
39
+ constructor(ms: number);
40
+ }
41
+ type RetryConfig = {
42
+ times: number;
43
+ waitBefore?: (attempt: number) => number;
44
+ };
45
+ type SafeHooks<T, E, TContext extends unknown[] = [], TOut = T> = {
46
+ parseResult?: (response: T) => TOut;
47
+ onSuccess?: (result: TOut, context: TContext) => void;
48
+ onError?: (error: E, context: TContext) => void;
49
+ onSettled?: (result: TOut | null, error: E | null, context: TContext) => void;
50
+ onHookError?: (error: unknown, hookName: string) => void;
51
+ /** Fallback error value returned when `parseError` throws. */
52
+ defaultError?: E;
53
+ };
54
+ type SafeAsyncHooks<T, E, TContext extends unknown[] = [], TOut = T> = SafeHooks<T, E, TContext, TOut> & {
55
+ onRetry?: (error: E, attempt: number, context: TContext) => void;
56
+ retry?: RetryConfig;
57
+ abortAfter?: number;
58
+ };
59
+ /**
60
+ * Configuration for creating a pre-configured safe instance
61
+ * @typeParam E - The error type that parseError returns
62
+ * @typeParam TResult - The return type of parseResult (inferred from parseResult)
63
+ */
64
+ type CreateSafeConfig<E, TResult = never> = {
65
+ /**
66
+ * Error mapping function applied to all caught errors.
67
+ *
68
+ * If `parseError` throws, the exception is caught and reported via `onHookError`
69
+ * (hookName `'parseError'`). The `defaultError` value is returned as the error
70
+ * result; if `defaultError` is not provided, the raw caught error is normalized
71
+ * to an `Error` instance via `new Error(String(e))`.
72
+ */
73
+ parseError: (e: unknown) => NonFalsy<E>;
74
+ /**
75
+ * Fallback error value returned when `parseError` throws.
76
+ * Must be provided alongside `parseError` in `createSafe`.
77
+ */
78
+ defaultError: E;
79
+ /** Optional response transform applied to all successful results. Per-call parseResult overrides this. */
80
+ parseResult?: (response: unknown) => TResult;
81
+ /** Optional default success hook (result is unknown since T varies per call) */
82
+ onSuccess?: (result: unknown) => void;
83
+ /** Optional default error hook (receives the mapped error type E) */
84
+ onError?: (error: E) => void;
85
+ /** Optional default settled hook — fires after success or error */
86
+ onSettled?: (result: unknown, error: E | null) => void;
87
+ /** Optional default retry hook for async operations */
88
+ onRetry?: (error: E, attempt: number) => void;
89
+ /** Optional default retry configuration for async operations */
90
+ retry?: RetryConfig;
91
+ /** Optional default timeout for all async operations in milliseconds */
92
+ abortAfter?: number;
93
+ /** Optional callback invoked when any hook throws. Receives the thrown error and the hook name. */
94
+ onHookError?: (error: unknown, hookName: string) => void;
95
+ };
96
+ /**
97
+ * A pre-configured safe instance with a fixed error type
98
+ * Methods do not accept parseError parameter (already configured)
99
+ * @typeParam E - The error type used by all methods
100
+ * @typeParam TResult - The factory parseResult return type (never = no factory parseResult)
101
+ */
102
+ type SafeInstance<E, TResult = never> = {
103
+ sync: <T, TOut = [TResult] extends [never] ? T : TResult>(fn: () => T, hooks?: SafeHooks<T, E, [], TOut>) => SafeResult<TOut, E>;
104
+ async: <T, TOut = [TResult] extends [never] ? T : TResult>(fn: (signal?: AbortSignal) => Promise<T>, hooks?: SafeAsyncHooks<T, E, [], TOut>) => Promise<SafeResult<TOut, E>>;
105
+ wrap: <TArgs extends unknown[], T, TOut = [TResult] extends [never] ? T : TResult>(fn: (...args: TArgs) => T, hooks?: SafeHooks<T, E, TArgs, TOut>) => (...args: TArgs) => SafeResult<TOut, E>;
106
+ wrapAsync: <TArgs extends unknown[], T, TOut = [TResult] extends [never] ? T : TResult>(fn: (...args: TArgs) => Promise<T>, hooks?: SafeAsyncHooks<T, E, TArgs, TOut>) => (...args: TArgs) => Promise<SafeResult<TOut, E>>;
107
+ all: <T extends Record<string, (signal?: AbortSignal) => Promise<any>>>(fns: T) => Promise<SafeResult<{
108
+ [K in keyof T]: [TResult] extends [never] ? T[K] extends (signal?: AbortSignal) => Promise<infer V> ? V : never : TResult;
109
+ }, E>>;
110
+ allSettled: <T extends Record<string, (signal?: AbortSignal) => Promise<any>>>(fns: T) => Promise<{
111
+ [K in keyof T]: SafeResult<[
112
+ TResult
113
+ ] extends [never] ? T[K] extends (signal?: AbortSignal) => Promise<infer V> ? V : never : TResult, E>;
114
+ }>;
115
+ };
116
+ /**
117
+ * Object-style variant of SafeInstance where all methods return SafeResultObj instead of SafeResult tuples.
118
+ * Created by wrapping a SafeInstance with withObjects().
119
+ */
120
+ type SafeObjectInstance<E, TResult = never> = {
121
+ sync: <T, TOut = [TResult] extends [never] ? T : TResult>(fn: () => T, hooks?: SafeHooks<T, E, [], TOut>) => SafeResultObj<TOut, E>;
122
+ async: <T, TOut = [TResult] extends [never] ? T : TResult>(fn: (signal?: AbortSignal) => Promise<T>, hooks?: SafeAsyncHooks<T, E, [], TOut>) => Promise<SafeResultObj<TOut, E>>;
123
+ wrap: <TArgs extends unknown[], T, TOut = [TResult] extends [never] ? T : TResult>(fn: (...args: TArgs) => T, hooks?: SafeHooks<T, E, TArgs, TOut>) => (...args: TArgs) => SafeResultObj<TOut, E>;
124
+ wrapAsync: <TArgs extends unknown[], T, TOut = [TResult] extends [never] ? T : TResult>(fn: (...args: TArgs) => Promise<T>, hooks?: SafeAsyncHooks<T, E, TArgs, TOut>) => (...args: TArgs) => Promise<SafeResultObj<TOut, E>>;
125
+ all: <T extends Record<string, (signal?: AbortSignal) => Promise<any>>>(fns: T) => Promise<SafeResultObj<{
126
+ [K in keyof T]: [TResult] extends [never] ? T[K] extends (signal?: AbortSignal) => Promise<infer V> ? V : never : TResult;
127
+ }, E>>;
128
+ allSettled: <T extends Record<string, (signal?: AbortSignal) => Promise<any>>>(fns: T) => Promise<{
129
+ [K in keyof T]: SafeResultObj<[
130
+ TResult
131
+ ] extends [never] ? T[K] extends (signal?: AbortSignal) => Promise<infer V> ? V : never : TResult, E>;
132
+ }>;
133
+ };
134
+
135
+ /**
136
+ * Execute a synchronous function and return a result tuple instead of throwing.
137
+ *
138
+ * Catches any error thrown by `fn` and returns `[null, error]`.
139
+ * On success, returns `[result, null]`.
140
+ *
141
+ * @param fn - The synchronous function to execute.
142
+ * @param parseError - Optional function to transform the caught error into a custom type `E`.
143
+ * If `parseError` throws, the exception is caught and reported via `onHookError`
144
+ * (hookName `'parseError'`). The `defaultError` value is returned if provided;
145
+ * otherwise the raw caught error is normalized to an `Error` instance.
146
+ * @param hooks - Optional hooks for side effects (`parseResult`, `onSuccess`, `onError`, `onSettled`).
147
+ * @returns A `SafeResult<T, E>` tuple: `[value, null]` on success or `[null, error]` on failure.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const [user, error] = safe.sync(() => JSON.parse(rawJson))
152
+ *
153
+ * // With custom error parsing and hooks
154
+ * const [value, err] = safe.sync(
155
+ * () => riskyOperation(),
156
+ * (e) => ({ code: 'PARSE_ERROR', message: String(e) }),
157
+ * { onError: (error) => console.error(error.code) }
158
+ * )
159
+ * ```
160
+ */
161
+ declare function safeSync<T>(fn: () => T): SafeResult<T, Error>;
162
+ declare function safeSync<T, TOut = T>(fn: () => T, hooks: SafeHooks<T, Error, [], TOut>): SafeResult<TOut, Error>;
163
+ declare function safeSync<T, E>(fn: () => T, parseError: (e: unknown) => NonFalsy<E>): SafeResult<T, E>;
164
+ declare function safeSync<T, E, TOut = T>(fn: () => T, parseError: (e: unknown) => NonFalsy<E>, hooks: SafeHooks<T, E, [], TOut> & {
165
+ defaultError: E;
166
+ }): SafeResult<TOut, E>;
167
+ /**
168
+ * Execute an asynchronous function and return a result tuple instead of throwing.
169
+ *
170
+ * Catches any error thrown or rejected by `fn` and returns `[null, error]`.
171
+ * On success, returns `[result, null]`. Supports retry and timeout via hooks.
172
+ *
173
+ * @param fn - The async function to execute. Receives an optional `AbortSignal` when `abortAfter` is configured.
174
+ * @param parseError - Optional function to transform the caught error into a custom type `E`.
175
+ * If `parseError` throws, the exception is caught and reported via `onHookError`
176
+ * (hookName `'parseError'`). The `defaultError` value is returned if provided;
177
+ * otherwise the raw caught error is normalized to an `Error` instance.
178
+ * @param hooks - Optional hooks including `retry`, `abortAfter`, `onRetry`, `parseResult`, `onSuccess`, `onError`, `onSettled`.
179
+ * @returns A `Promise<SafeResult<T, E>>` tuple: `[value, null]` on success or `[null, error]` on failure.
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * const [data, error] = await safe.async(() => fetch('/api/users').then(r => r.json()))
184
+ *
185
+ * // With retry and timeout
186
+ * const [data, error] = await safe.async(
187
+ * (signal) => fetch('/api/users', { signal }).then(r => r.json()),
188
+ * { retry: { times: 3 }, abortAfter: 5000 }
189
+ * )
190
+ * ```
191
+ */
192
+ declare function safeAsync<T>(fn: (signal?: AbortSignal) => Promise<T>): Promise<SafeResult<T, Error>>;
193
+ declare function safeAsync<T, TOut = T>(fn: (signal?: AbortSignal) => Promise<T>, hooks: SafeAsyncHooks<T, Error, [], TOut>): Promise<SafeResult<TOut, Error>>;
194
+ declare function safeAsync<T, E>(fn: (signal?: AbortSignal) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>): Promise<SafeResult<T, E>>;
195
+ declare function safeAsync<T, E, TOut = T>(fn: (signal?: AbortSignal) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>, hooks: SafeAsyncHooks<T, E, [], TOut> & {
196
+ defaultError: E;
197
+ }): Promise<SafeResult<TOut, E>>;
198
+ /**
199
+ * Wrap a synchronous function so it returns a result tuple instead of throwing.
200
+ *
201
+ * Returns a new function with the same parameter signature that catches
202
+ * errors and returns `[null, error]` instead of throwing. The original
203
+ * arguments are passed through to hooks as context.
204
+ *
205
+ * @param fn - The synchronous function to wrap.
206
+ * @param parseError - Optional function to transform the caught error into a custom type `E`.
207
+ * If `parseError` throws, the exception is caught and reported via `onHookError`
208
+ * (hookName `'parseError'`). The `defaultError` value is returned if provided;
209
+ * otherwise the raw caught error is normalized to an `Error` instance.
210
+ * @param hooks - Optional hooks for side effects (`parseResult`, `onSuccess`, `onError`, `onSettled`). Hooks receive the original call arguments as context.
211
+ * @returns A wrapped function `(...args) => SafeResult<T, E>`.
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const safeJsonParse = safe.wrap(JSON.parse)
216
+ * const [data, error] = safeJsonParse('{"valid": true}')
217
+ *
218
+ * // With hooks that receive the original arguments
219
+ * const safeDivide = safe.wrap(
220
+ * (a: number, b: number) => { if (b === 0) throw new Error('Division by zero'); return a / b },
221
+ * { onError: (error, [a, b]) => console.error(`Failed to divide ${a} by ${b}`) }
222
+ * )
223
+ * ```
224
+ */
225
+ declare function wrap<TArgs extends unknown[], T>(fn: (...args: TArgs) => T): (...args: TArgs) => SafeResult<T, Error>;
226
+ declare function wrap<TArgs extends unknown[], T, TOut = T>(fn: (...args: TArgs) => T, hooks: SafeHooks<T, Error, TArgs, TOut>): (...args: TArgs) => SafeResult<TOut, Error>;
227
+ declare function wrap<TArgs extends unknown[], T, E>(fn: (...args: TArgs) => T, parseError: (e: unknown) => NonFalsy<E>): (...args: TArgs) => SafeResult<T, E>;
228
+ declare function wrap<TArgs extends unknown[], T, E, TOut = T>(fn: (...args: TArgs) => T, parseError: (e: unknown) => NonFalsy<E>, hooks: SafeHooks<T, E, TArgs, TOut> & {
229
+ defaultError: E;
230
+ }): (...args: TArgs) => SafeResult<TOut, E>;
231
+ /**
232
+ * Wrap an asynchronous function so it returns a result tuple instead of throwing.
233
+ *
234
+ * Returns a new function with the same parameter signature that catches
235
+ * errors and returns `[null, error]` instead of throwing. Supports retry
236
+ * and timeout via hooks.
237
+ *
238
+ * **Note on `abortAfter`:** When configured, `abortAfter` acts as an external
239
+ * deadline — the promise is rejected with a `TimeoutError` after the specified
240
+ * duration, but the wrapped function does **not** receive an `AbortSignal`.
241
+ * This means the underlying operation will continue running in the background
242
+ * even after the timeout fires. If you need cooperative cancellation (e.g.
243
+ * passing a signal to `fetch`), use {@link safeAsync | safe.async} instead,
244
+ * which passes the signal directly to the function:
245
+ *
246
+ * ```typescript
247
+ * // Cooperative cancellation with safe.async
248
+ * const [data, error] = await safe.async(
249
+ * (signal) => fetch('/api/data', { signal }),
250
+ * { abortAfter: 5000 }
251
+ * )
252
+ * ```
253
+ *
254
+ * @param fn - The async function to wrap.
255
+ * @param parseError - Optional function to transform the caught error into a custom type `E`.
256
+ * If `parseError` throws, the exception is caught and reported via `onHookError`
257
+ * (hookName `'parseError'`). The `defaultError` value is returned if provided;
258
+ * otherwise the raw caught error is normalized to an `Error` instance.
259
+ * @param hooks - Optional hooks including `retry`, `abortAfter`, `onRetry`, `parseResult`, `onSuccess`, `onError`, `onSettled`. Hooks receive the original call arguments as context.
260
+ * @returns A wrapped function `(...args) => Promise<SafeResult<T, E>>`.
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * const safeFetchUser = safe.wrapAsync(
265
+ * (id: string) => fetch(`/api/users/${id}`).then(r => r.json())
266
+ * )
267
+ * const [user, error] = await safeFetchUser('123')
268
+ *
269
+ * // With retry
270
+ * const safeFetch = safe.wrapAsync(
271
+ * (url: string) => fetch(url).then(r => r.json()),
272
+ * { retry: { times: 3, waitBefore: (attempt) => attempt * 1000 } }
273
+ * )
274
+ * ```
275
+ */
276
+ declare function wrapAsync<TArgs extends unknown[], T>(fn: (...args: TArgs) => Promise<T>): (...args: TArgs) => Promise<SafeResult<T, Error>>;
277
+ declare function wrapAsync<TArgs extends unknown[], T, TOut = T>(fn: (...args: TArgs) => Promise<T>, hooks: SafeAsyncHooks<T, Error, TArgs, TOut>): (...args: TArgs) => Promise<SafeResult<TOut, Error>>;
278
+ declare function wrapAsync<TArgs extends unknown[], T, E>(fn: (...args: TArgs) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>): (...args: TArgs) => Promise<SafeResult<T, E>>;
279
+ declare function wrapAsync<TArgs extends unknown[], T, E, TOut = T>(fn: (...args: TArgs) => Promise<T>, parseError: (e: unknown) => NonFalsy<E>, hooks: SafeAsyncHooks<T, E, TArgs, TOut> & {
280
+ defaultError: E;
281
+ }): (...args: TArgs) => Promise<SafeResult<TOut, E>>;
282
+ /**
283
+ * Run multiple safe-wrapped async operations in parallel and return all values or the first error.
284
+ *
285
+ * Short-circuits: returns immediately when any operation fails, without waiting
286
+ * for remaining operations to settle. If all succeed, returns
287
+ * `ok({ key: value, ... })` with unwrapped values. If any fail, returns `err(firstError)`.
288
+ *
289
+ * @param promises - An object map of `Promise<SafeResult<T, E>>` entries.
290
+ * @returns A `Promise<SafeResult<{ [K]: V }, E>>` — all values on success, first error on failure.
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const [data, error] = await safe.all({
295
+ * user: safe.async(() => fetchUser()),
296
+ * posts: safe.async(() => fetchPosts()),
297
+ * })
298
+ * if (error) return handleError(error)
299
+ * data.user // User
300
+ * data.posts // Post[]
301
+ * ```
302
+ */
303
+ declare function safeAll<T extends Record<string, Promise<SafeResult<any, any>>>>(promises: T): Promise<SafeResult<{
304
+ [K in keyof T]: T[K] extends Promise<SafeResult<infer V, any>> ? V : never;
305
+ }, T[keyof T] extends Promise<SafeResult<any, infer E>> ? E : never>>;
306
+ /**
307
+ * Run multiple safe-wrapped async operations in parallel and return all individual results.
308
+ *
309
+ * Accepts an object map of `Promise<SafeResult>` entries. Always returns all results
310
+ * as named SafeResult entries — never fails at the group level.
311
+ *
312
+ * @param promises - An object map of `Promise<SafeResult<T, E>>` entries.
313
+ * @returns A `Promise<{ [K]: SafeResult<V, E> }>` — each key maps to its individual result.
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * const results = await safe.allSettled({
318
+ * user: safe.async(() => fetchUser()),
319
+ * posts: safe.async(() => fetchPosts()),
320
+ * })
321
+ * if (results.user.ok) {
322
+ * results.user.value // User
323
+ * }
324
+ * if (!results.posts.ok) {
325
+ * results.posts.error // Error
326
+ * }
327
+ * ```
328
+ */
329
+ declare function safeAllSettled<T extends Record<string, Promise<SafeResult<any, any>>>>(promises: T): Promise<{
330
+ [K in keyof T]: Awaited<T[K]>;
331
+ }>;
332
+ declare const safe: {
333
+ readonly sync: typeof safeSync;
334
+ readonly async: typeof safeAsync;
335
+ readonly wrap: typeof wrap;
336
+ readonly wrapAsync: typeof wrapAsync;
337
+ readonly all: typeof safeAll;
338
+ readonly allSettled: typeof safeAllSettled;
339
+ };
340
+
341
+ /**
342
+ * Create a pre-configured safe instance with a fixed error mapping function
343
+ *
344
+ * Returns a new safe object where all methods use the configured parseError.
345
+ * The error type E is automatically inferred from the parseError return type.
346
+ * If parseResult is provided, TResult is inferred from its return type
347
+ * and becomes the default result type for all methods.
348
+ *
349
+ * @example
350
+ * ```typescript
351
+ * const appSafe = createSafe({
352
+ * parseError: (e) => ({
353
+ * code: 'UNKNOWN_ERROR',
354
+ * message: e instanceof Error ? e.message : 'Unknown error',
355
+ * }),
356
+ * onError: (error) => logger.error(error.code),
357
+ * })
358
+ *
359
+ * const [result, error] = appSafe.sync(() => JSON.parse(data))
360
+ * // error is typed as { code: string; message: string }
361
+ *
362
+ * // With parseResult for runtime validation:
363
+ * const validatedSafe = createSafe({
364
+ * parseError: (e) => toAppError(e),
365
+ * parseResult: (response) => schema.parse(response),
366
+ * })
367
+ * // All methods return SafeResult<z.infer<typeof schema>, AppError>
368
+ * ```
369
+ */
370
+ declare function createSafe<E, TResult = never>(config: CreateSafeConfig<E, TResult>): SafeInstance<E, TResult>;
371
+
372
+ declare function withObjects<T, E>(result: SafeResult<T, E>): SafeResultObj<T, E>;
373
+ declare function withObjects<T, E>(result: Promise<SafeResult<T, E>>): Promise<SafeResultObj<T, E>>;
374
+ declare function withObjects<A extends unknown[], T, E>(fn: (...args: A) => SafeResult<T, E>): (...args: A) => SafeResultObj<T, E>;
375
+ declare function withObjects<A extends unknown[], T, E>(fn: (...args: A) => Promise<SafeResult<T, E>>): (...args: A) => Promise<SafeResultObj<T, E>>;
376
+ declare function withObjects<E, TResult>(instance: SafeInstance<E, TResult>): SafeObjectInstance<E, TResult>;
377
+
378
+ export { type CreateSafeConfig, type NonFalsy, type RetryConfig, type SafeAsyncHooks, type SafeErr, type SafeErrObj, type SafeHooks, type SafeInstance, type SafeObjectInstance, type SafeOk, type SafeOkObj, type SafeResult, type SafeResultObj, TimeoutError, createSafe, err, errObj, ok, okObj, safe, withObjects };