Skip to content

Instantly share code, notes, and snippets.

@sillvva
Last active August 7, 2025 20:01
Show Gist options
  • Select an option

  • Save sillvva/f16c88b65f8816d80ae1c3e6b6caef26 to your computer and use it in GitHub Desktop.

Select an option

Save sillvva/f16c88b65f8816d80ae1c3e6b6caef26 to your computer and use it in GitHub Desktop.
Effect + Sveltekit
import { dev } from "$app/environment";
import { getRequestEvent } from "$app/server";
import { privateEnv } from "$lib/env/private";
import type { AppLogSchema, UserId } from "$lib/schemas";
import { db, runQuery } from "$lib/server/db";
import { appLogs } from "$lib/server/db/schema";
import { removeTrace } from "$lib/util";
import { isInstanceOfClass } from "@sillvva/utils";
import { error, isHttpError, isRedirect, type NumericRange } from "@sveltejs/kit";
import { Cause, Effect, Exit, HashMap, Layer, Logger } from "effect";
import { isFunction, isTupleOf } from "effect/Predicate";
import type { YieldWrap } from "effect/Utils";
// -------------------------------------------------------------------------------------------------
// Logs
// -------------------------------------------------------------------------------------------------
export type Annotations = {
routeId: string | null;
params: Partial<Record<string, string>>;
userId: UserId | undefined;
username: string | undefined;
extra: object;
};
const logLevel = Logger.withMinimumLogLevel(privateEnv.LOG_LEVEL);
const dbLogger = Logger.replace(
Logger.defaultLogger,
Logger.make((log) => {
const values = {
label: (log.message as string[]).join(" | "),
timestamp: log.date,
level: log.logLevel.label,
annotations: Object.fromEntries(HashMap.toEntries(log.annotations)) as Annotations
} satisfies Omit<AppLogSchema, "id">;
runQuery(db.insert(appLogs).values([values]).returning({ id: appLogs.id })).pipe(
Effect.andThen((logs) => {
if (dev && isTupleOf(logs, 1))
console.log(logs[0].id, dev && ["ERROR", "DEBUG"].includes(values.level) ? values : JSON.stringify(values), "\n");
}),
Effect.runPromise
);
})
);
function annotate(extra: Record<PropertyKey, any> = {}) {
const event = getRequestEvent();
return Effect.annotateLogs({
userId: event.locals.user?.id,
username: event.locals.user?.name,
routeId: event.route.id,
params: event.params,
extra
} satisfies Annotations);
}
export const Log = {
info: (message: string, extra?: Record<PropertyKey, any>, logger: Layer.Layer<never> = dbLogger) =>
Effect.logInfo(message).pipe(logLevel, annotate(extra), Effect.provide(logger)),
error: (message: string, extra?: Record<PropertyKey, any>, logger: Layer.Layer<never> = dbLogger) =>
Effect.logError(message).pipe(logLevel, annotate(extra), Effect.provide(logger)),
debug: (message: string, extra?: Record<PropertyKey, any>, logger: Layer.Layer<never> = dbLogger) =>
Effect.logDebug(message).pipe(logLevel, annotate(extra), Effect.provide(logger))
};
// -------------------------------------------------------------------------------------------------
// Run
// -------------------------------------------------------------------------------------------------
// Overload signatures
export async function runOrThrow<A, B extends InstanceType<ErrorClass>, T extends YieldWrap<Effect.Effect<A, B>>, X, Y>(
program: () => Generator<T, X, Y>
): Promise<X>;
export async function runOrThrow<A, B extends InstanceType<ErrorClass>>(
program: Effect.Effect<A, B> | (() => Effect.Effect<A, B>)
): Promise<A>;
// Implementation
export async function runOrThrow<A, B extends InstanceType<ErrorClass>, T extends YieldWrap<Effect.Effect<A, B>>, X, Y>(
program: Effect.Effect<A, B> | (() => Effect.Effect<A, B>) | (() => Generator<T, X, Y>)
): Promise<A | X> {
const effect = Effect.fn(function* () {
if (isFunction(program)) {
return yield* program();
} else {
return yield* program;
}
});
const result = await Effect.runPromiseExit(effect());
return Exit.match(result, {
onSuccess: (result) => result,
onFailure: (cause) => {
const { message, status } = handleCause(cause);
throw error(status, message);
}
});
}
export type EffectSuccess<A> = { ok: true; data: A };
export type EffectFailure = {
ok: false;
error: { message: string; status: NumericRange<400, 599>; extra: Record<string, unknown> };
};
export type EffectResult<A> = EffectSuccess<A> | EffectFailure;
// Overload signatures
export async function runOrReturn<A, B extends InstanceType<ErrorClass>, T extends YieldWrap<Effect.Effect<A, B>>, X, Y>(
program: () => Generator<T, X, Y>
): Promise<EffectResult<X>>;
export async function runOrReturn<A, B extends InstanceType<ErrorClass>>(
program: Effect.Effect<A, B> | (() => Effect.Effect<A, B>)
): Promise<EffectResult<A>>;
// Implementation
export async function runOrReturn<A, B extends InstanceType<ErrorClass>, T extends YieldWrap<Effect.Effect<A, B>>, X, Y>(
program: Effect.Effect<A, B> | (() => Effect.Effect<A, B>) | (() => Generator<T, X, Y>)
): Promise<EffectResult<A | X>> {
const effect = Effect.fn(function* () {
if (isFunction(program)) {
return yield* program();
} else {
return yield* program;
}
});
const result = await Effect.runPromiseExit(effect());
return Exit.match(result, {
onSuccess: (result) => ({ ok: true, data: result }),
onFailure: (cause) => ({ ok: false, error: handleCause(cause) })
});
}
function handleCause<B extends InstanceType<ErrorClass>>(cause: Cause.Cause<B>) {
let message = Cause.pretty(cause);
let status: NumericRange<400, 599> = 500;
const extra: Record<string, unknown> = {};
if (Cause.isFailType(cause)) {
const error = cause.error;
status = error.status;
for (const key in error) {
if (!["_tag", "_op", "pipe", "name"].includes(key)) {
extra[key] = error[key];
}
}
}
if (Cause.isDieType(cause)) {
const defect = cause.defect;
// This will propagate redirects and http errors directly to SvelteKit
if (isRedirect(defect)) {
Effect.runFork(AppLog.info(`Redirect to ${defect.location}`, defect));
throw defect;
} else if (isHttpError(defect)) {
Effect.runFork(AppLog.error(`HttpError [${defect.status}] ${defect.body.message}`, defect));
throw defect;
} else if (typeof defect === "object" && defect !== null && "stack" in defect) {
extra.stack = defect.stack;
}
}
Effect.runFork(AppLog.error(message, extra));
if (!dev) message = removeTrace(message);
return { message, status, extra };
}
// -------------------------------------------------------------------------------------------------
// Errors
// -------------------------------------------------------------------------------------------------
export interface ErrorParams {
message: string;
status: NumericRange<400, 599>;
cause?: unknown;
[key: string]: unknown;
}
export interface ErrorClass {
new (...args: any[]): { _tag: string } & ErrorParams;
}
export function isTaggedError(error: unknown): error is InstanceType<ErrorClass> {
return (
isInstanceOfClass(error) &&
"status" in error &&
typeof error.status === "number" &&
error.status >= 400 &&
error.status <= 599 &&
"cause" in error &&
"message" in error &&
typeof error.message === "string" &&
"_tag" in error &&
typeof error._tag === "string"
);
}
// Example Usage
/*
export class CharacterNotFoundError extends Data.TaggedError("CharacterNotFoundError")<ErrorParams> {
constructor(err?: unknown) {
super({ message: "Character not found", status: 404, cause: err });
}
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment