Files
2026-04-05 03:08:53 +02:00

737 lines
18 KiB
JavaScript

//#region src/dual.ts
/**
* Creates data-first/data-last dual function.
*
* @template DataLast Curried (data-last) signature.
* @template DataFirst Uncurried (data-first) signature.
* @param arity Number of args for data-first form.
* @param body Implementation function.
* @returns Function supporting both calling conventions.
*
* @example
* const add: {
* (a: number, b: number): number;
* (b: number): (a: number) => number;
* } = dual(2, (a: number, b: number) => a + b);
*
* add(1, 2); // 3 (data-first)
* add(2)(1); // 3 (data-last)
*/
function dual(arity, body) {
if (arity === 2) return ((...args) => {
if (args.length >= 2) return body(args[0], args[1]);
return (self) => body(self, args[0]);
});
if (arity === 3) return ((...args) => {
if (args.length >= 3) return body(args[0], args[1], args[2]);
return (self) => body(self, args[0], args[1]);
});
if (arity === 4) return ((...args) => {
if (args.length >= 4) return body(args[0], args[1], args[2], args[3]);
return (self) => body(self, args[0], args[1], args[2]);
});
return ((...args) => {
if (args.length >= arity) return body(...args);
return (self) => body(self, ...args);
});
}
//#endregion
//#region src/error.ts
/** Serialize cause for JSON output */
const serializeCause = (cause) => {
if (cause instanceof Error) return {
name: cause.name,
message: cause.message,
stack: cause.stack
};
return cause;
};
/** Type guard for any tagged error */
const isAnyTaggedError = (value) => {
return value instanceof Error && "_tag" in value && typeof value._tag === "string";
};
/**
* Factory for tagged error classes.
*
* @example
* class NotFoundError extends TaggedError("NotFoundError")<{
* id: string;
* message: string;
* }>() {}
*
* const err = new NotFoundError({ id: "123", message: "Not found: 123" });
* err._tag // "NotFoundError"
* err.id // "123"
* err.message // "Not found: 123"
*
* // Check if any tagged error
* TaggedError.is(err) // true
*/
const TaggedError = Object.assign((tag) => () => {
class Base extends Error {
_tag = tag;
/** Type guard for this error class */
static is(value) {
return value instanceof Base;
}
constructor(args) {
const message = args && "message" in args && typeof args.message === "string" ? args.message : void 0;
const cause = args && "cause" in args ? args.cause : void 0;
super(message, cause !== void 0 ? { cause } : void 0);
if (args) Object.assign(this, args);
Object.setPrototypeOf(this, new.target.prototype);
this.name = tag;
if (cause instanceof Error && cause.stack) {
const indented = cause.stack.replace(/\n/g, "\n ");
this.stack = `${this.stack}\nCaused by: ${indented}`;
}
}
toJSON() {
return {
...this,
_tag: this._tag,
name: this.name,
message: this.message,
cause: serializeCause(this.cause),
stack: this.stack
};
}
}
return Base;
}, { is: isAnyTaggedError });
/**
* Exhaustive pattern match on tagged error union.
*
* @example
* // Data-first
* matchError(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* });
*
* // Data-last (pipeable)
* pipe(err, matchError({
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* }));
*/
const matchError = dual(2, (err$1, handlers) => {
const handler = handlers[err$1._tag];
return handler(err$1);
});
/**
* Partial pattern match with fallback for unhandled tags.
*
* @example
* matchErrorPartial(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* }, (e) => `Unknown: ${e.message}`);
*/
const matchErrorPartial = dual(3, (err$1, handlers, fallback) => {
const handler = handlers[err$1._tag];
if (typeof handler === "function") return handler(err$1);
return fallback(err$1);
});
/**
* Type guard for tagged error instances.
*
* @example
* if (isTaggedError(value)) { value._tag }
*/
const isTaggedError = isAnyTaggedError;
/**
* Wraps exceptions caught by Result.try/tryPromise.
* Custom constructor derives message from cause.
*/
var UnhandledException = class extends TaggedError("UnhandledException")() {
constructor(args) {
const message = args.cause instanceof Error ? `Unhandled exception: ${args.cause.message}` : `Unhandled exception: ${String(args.cause)}`;
super({
message,
cause: args.cause
});
}
};
/**
* Unrecoverable error — user code threw inside Result operations.
*
* @example
* // Panic in generator cleanup:
* Result.gen(function* () {
* try {
* yield* Result.err("expected error");
* } finally {
* throw new Error("cleanup failed"); // Panic!
* }
* });
*
* // Panic in combinator:
* Result.ok(1).map(() => { throw new Error("oops"); }); // Panic!
*/
var Panic = class extends TaggedError("Panic")() {};
/**
* Returned when Result.deserialize receives invalid input.
*
* @example
* const result = Result.deserialize(invalidData);
* if (Result.isError(result) && ResultDeserializationError.is(result.error)) {
* console.log("Invalid input:", result.error.value);
* }
*/
var ResultDeserializationError = class extends TaggedError("ResultDeserializationError")() {
constructor(args) {
super({
message: `Failed to deserialize value as Result: expected { status: "ok", value } or { status: "error", error }`,
value: args.value
});
}
};
/**
* Type guard for Panic instances.
*
* @example
* if (isPanic(value)) { value.cause }
*/
const isPanic = (value) => {
return value instanceof Panic;
};
/**
* Throw an unrecoverable Panic.
*
* @example
* panic("something went wrong", cause);
*/
const panic = (message, cause) => {
throw new Panic({
message,
cause
});
};
//#endregion
//#region src/result.ts
/** Executes fn, panics if it throws. */
const tryOrPanic = (fn, message) => {
try {
return fn();
} catch (cause) {
throw panic(message, cause);
}
};
/** Async version of tryOrPanic. */
const tryOrPanicAsync = async (fn, message) => {
try {
return await fn();
} catch (cause) {
throw panic(message, cause);
}
};
/**
* Successful result variant.
*
* @template A Success value type.
* @template E Error type (phantom - for type unification).
*
* @example
* const result = new Ok(42);
* result.value // 42
* result.status // "ok"
*/
var Ok = class Ok {
status = "ok";
constructor(value) {
this.value = value;
}
/** Returns true, narrowing Result to Ok. */
isOk() {
return true;
}
/** Returns false, narrowing Result to Err. */
isErr() {
return false;
}
/**
* Transforms success value.
*
* @template B Transformed type.
* @param fn Transformation function.
* @returns Ok with transformed value.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).map(x => x * 2) // Ok(4)
*/
map(fn) {
return tryOrPanic(() => new Ok(fn(this.value)), "map callback threw");
}
/**
* No-op on Ok, returns self with new phantom error type.
*
* @template E2 New error type.
* @param _fn Ignored.
* @returns Self with updated phantom E type.
*/
mapError(_fn) {
return this;
}
/**
* Chains Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Function returning Result.
* @returns Result from fn.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).andThen(x => x > 0 ? ok(x) : err("negative")) // Ok(2)
*/
andThen(fn) {
return tryOrPanic(() => fn(this.value), "andThen callback threw");
}
/**
* Chains async Result-returning function.
*
* @template B New success type.
* @template E2 New error type.
* @param fn Async function returning Result.
* @returns Promise of Result from fn.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(1).andThenAsync(async x => ok(await fetchData(x)))
*/
andThenAsync(fn) {
return tryOrPanicAsync(() => fn(this.value), "andThenAsync callback threw");
}
/**
* Pattern matches on Result.
*
* @template T Return type.
* @param handlers Ok and err handlers.
* @returns Result of ok handler.
* @throws {Panic} If handler throws.
*
* @example
* ok(2).match({ ok: x => x * 2, err: () => 0 }) // 4
*/
match(handlers) {
return tryOrPanic(() => handlers.ok(this.value), "match ok handler threw");
}
/**
* Extracts value.
*
* @param _message Ignored.
* @returns The value.
*
* @example
* ok(42).unwrap() // 42
*/
unwrap(_message) {
return this.value;
}
/**
* Returns value, ignoring fallback.
*
* @template B Fallback type.
* @param _fallback Ignored.
* @returns The value.
*
* @example
* ok(42).unwrapOr(0) // 42
*/
unwrapOr(_fallback) {
return this.value;
}
/**
* Runs side effect, returns self.
*
* @param fn Side effect function.
* @returns Self.
* @throws {Panic} If fn throws.
*
* @example
* ok(2).tap(console.log).map(x => x * 2) // logs 2, returns Ok(4)
*/
tap(fn) {
return tryOrPanic(() => {
fn(this.value);
return this;
}, "tap callback threw");
}
/**
* Runs async side effect, returns self.
*
* @param fn Async side effect function.
* @returns Promise of self.
* @throws {Panic} If fn throws synchronously or rejects.
*
* @example
* await ok(2).tapAsync(async x => await log(x))
*/
tapAsync(fn) {
return tryOrPanicAsync(async () => {
await fn(this.value);
return this;
}, "tapAsync callback threw");
}
/**
* Makes Ok yieldable in Result.gen blocks.
* Immediately returns the value without yielding.
* Yield type Err<never, E> matches Err's for proper union inference.
*/
*[Symbol.iterator]() {
return this.value;
}
};
/**
* Error result variant.
*
* @template T Success type (phantom - for type unification with Ok).
* @template E Error value type.
*
* @example
* const result = new Err("failed");
* result.error // "failed"
* result.status // "error"
*/
var Err = class Err {
status = "error";
constructor(error) {
this.error = error;
}
/** Returns false, narrowing Result to Ok. */
isOk() {
return false;
}
/** Returns true, narrowing Result to Err. */
isErr() {
return true;
}
/**
* No-op on Err, returns self with new phantom T.
*
* @template U New phantom success type.
* @param _fn Ignored.
* @returns Self.
*/
map(_fn) {
return this;
}
/**
* Transforms error value.
*
* @template E2 Transformed error type.
* @param fn Transformation function.
* @returns Err with transformed error.
* @throws {Panic} If fn throws.
*
* @example
* err("fail").mapError(e => new Error(e)) // Err(Error("fail"))
*/
mapError(fn) {
return tryOrPanic(() => new Err(fn(this.error)), "mapError callback threw");
}
/**
* No-op on Err, returns self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Self.
*/
andThen(_fn) {
return this;
}
/**
* No-op on Err, returns Promise of self with widened error type.
*
* @template U New phantom success type.
* @template E2 Additional error type.
* @param _fn Ignored.
* @returns Promise of self.
*/
andThenAsync(_fn) {
return Promise.resolve(this);
}
/**
* Pattern matches on Result.
*
* @template R Return type.
* @param handlers Ok and err handlers.
* @returns Result of err handler.
* @throws {Panic} If handler throws.
*
* @example
* err("fail").match({ ok: x => x, err: e => e.length }) // 4
*/
match(handlers) {
return tryOrPanic(() => handlers.err(this.error), "match err handler threw");
}
/**
* Throws error with optional message.
*
* @param message Error message.
* @throws Always throws.
*
* @example
* err("fail").unwrap() // throws Error
* err("fail").unwrap("custom") // throws Error("custom")
*/
unwrap(message) {
return panic(message ?? `Unwrap called on Err: ${String(this.error)}`, this.error);
}
/**
* Returns fallback value.
*
* @template U Fallback type.
* @param fallback Fallback value.
* @returns Fallback.
*
* @example
* err("fail").unwrapOr(42) // 42
*/
unwrapOr(fallback) {
return fallback;
}
/**
* No-op on Err, returns self.
*
* @param _fn Ignored.
* @returns Self.
*/
tap(_fn) {
return this;
}
/**
* No-op on Err, returns Promise of self.
*
* @param _fn Ignored.
* @returns Promise of self.
*/
tapAsync(_fn) {
return Promise.resolve(this);
}
/**
* Makes Err yieldable in Result.gen blocks.
* Yields Err<never, E> for proper union inference across multiple yields.
*/
*[Symbol.iterator]() {
yield this;
return panic("Unreachable: Err yielded in Result.gen but generator continued", this.error);
}
};
function ok(value) {
return new Ok(value);
}
const isOk = (result) => {
return result.status === "ok";
};
const err = (error) => new Err(error);
const isError = (result) => {
return result.status === "error";
};
const tryFn = (options, config) => {
const execute = () => {
if (typeof options === "function") try {
return ok(options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
try {
return ok(options.try());
} catch (originalCause) {
try {
return err(options.catch(originalCause));
} catch (catchHandlerError) {
throw panic("Result.try catch handler threw", catchHandlerError);
}
}
};
const times = config?.retry?.times ?? 0;
let result = execute();
for (let retry = 0; retry < times && result.status === "error"; retry++) result = execute();
return result;
};
const tryPromise = async (options, config) => {
const execute = async () => {
if (typeof options === "function") try {
return ok(await options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
try {
return ok(await options.try());
} catch (originalCause) {
try {
return err(await options.catch(originalCause));
} catch (catchHandlerError) {
throw panic("Result.tryPromise catch handler threw", catchHandlerError);
}
}
};
const retry = config?.retry;
if (!retry) return execute();
const getDelay = (retryAttempt) => {
switch (retry.backoff) {
case "constant": return retry.delayMs;
case "linear": return retry.delayMs * (retryAttempt + 1);
case "exponential": return retry.delayMs * 2 ** retryAttempt;
}
};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let result = await execute();
const shouldRetryFn = retry.shouldRetry ?? (() => true);
for (let attempt = 0; attempt < retry.times; attempt++) {
if (result.status !== "error") break;
const error = result.error;
if (!tryOrPanic(() => shouldRetryFn(error), "shouldRetry predicate threw")) break;
await sleep(getDelay(attempt));
result = await execute();
}
return result;
};
const map = dual(2, (result, fn) => {
return result.map(fn);
});
const mapError = dual(2, (result, fn) => {
return result.mapError(fn);
});
const andThen = dual(2, (result, fn) => {
return result.andThen(fn);
});
const andThenAsync = dual(2, (result, fn) => {
return result.andThenAsync(fn);
});
const match = dual(2, (result, handlers) => {
return result.match(handlers);
});
const tap = dual(2, (result, fn) => {
return result.tap(fn);
});
const tapAsync = dual(2, (result, fn) => {
return result.tapAsync(fn);
});
const unwrap = (result, message) => {
return result.unwrap(message);
};
/** Validates that a value is a Result instance. Throws with helpful message if not. */
function assertIsResult(value) {
if (value !== null && typeof value === "object" && "status" in value && (value.status === "ok" || value.status === "error")) return;
return panic("Result.gen body must return Result.ok() or Result.err(), got: " + (value === null ? "null" : typeof value === "object" ? JSON.stringify(value) : String(value)));
}
const unwrapOr = dual(2, (result, fallback) => {
return result.unwrapOr(fallback);
});
const gen = ((body, thisArg) => {
const iterator = body.call(thisArg);
if (Symbol.asyncIterator in iterator) return (async () => {
const asyncIter = iterator;
let state$1;
try {
state$1 = await asyncIter.next();
} catch (cause) {
throw panic("generator body threw", cause);
}
assertIsResult(state$1.value);
if (!state$1.done) try {
await asyncIter.return?.(void 0);
} catch (cause) {
throw panic("generator cleanup threw", cause);
}
return state$1.value;
})();
const syncIter = iterator;
let state;
try {
state = syncIter.next();
} catch (cause) {
throw panic("generator body threw", cause);
}
assertIsResult(state.value);
if (!state.done) try {
syncIter.return?.(void 0);
} catch (cause) {
throw panic("generator cleanup threw", cause);
}
return state.value;
});
async function* resultAwait(promise) {
return yield* await promise;
}
function isSerializedResult(obj) {
return obj !== null && typeof obj === "object" && "status" in obj && (obj.status === "ok" && "value" in obj || obj.status === "error" && "error" in obj);
}
const serialize = (result) => {
return result.status === "ok" ? {
status: "ok",
value: result.value
} : {
status: "error",
error: result.error
};
};
const deserialize = (value) => {
if (isSerializedResult(value)) return value.status === "ok" ? new Ok(value.value) : new Err(value.error);
return err(new ResultDeserializationError({ value }));
};
/**
* @deprecated Use `Result.deserialize` instead. Will be removed in 3.0.
*/
const hydrate = (value) => {
return deserialize(value);
};
const partition = (results) => {
const oks = [];
const errs = [];
for (const r of results) if (r.status === "ok") oks.push(r.value);
else errs.push(r.error);
return [oks, errs];
};
/**
* Flattens nested Result into single Result.
*
* @example
* const nested: Result<Result<number, E1>, E2> = Result.ok(Result.ok(42));
* const flat: Result<number, E1 | E2> = Result.flatten(nested); // Ok(42)
*/
const flatten = (result) => {
if (result.status === "ok") return result.value;
return result;
};
/**
* Utilities for creating and handling Result types.
*
* @example
* const result = Result.try(() => JSON.parse(str));
* const value = result.map(x => x.id).unwrapOr("default");
*/
const Result = {
ok,
isOk,
err,
isError,
try: tryFn,
tryPromise,
map,
mapError,
andThen,
andThenAsync,
match,
tap,
tapAsync,
unwrap,
unwrapOr,
gen,
await: resultAwait,
serialize,
deserialize,
hydrate,
partition,
flatten
};
//#endregion
export { Err, Ok, Panic, Result, ResultDeserializationError, TaggedError, UnhandledException, isPanic, isTaggedError, matchError, matchErrorPartial, panic };
//# sourceMappingURL=index.mjs.map