@kuroski/effect-svelte 1.0.0

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.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # effect-svelte
2
+
3
+ Integration library for [Effect](https://effect.website/) with [SvelteKit](https://kit.svelte.dev/). Run Effect programs in SvelteKit load functions and remote functions with automatic error handling, redirects, and form validation.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm add effect-svelte
9
+ # or
10
+ bun add effect-svelte
11
+ ```
12
+
13
+ **Peer dependencies:** `effect >=3.19.15`, `svelte >=5`, `@sveltejs/kit >=2`
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Create a Runner
18
+
19
+ ```ts
20
+ // src/lib/server/runtime.ts
21
+ import { Effect, Layer, ManagedRuntime } from "effect";
22
+ import { createRunner } from "effect-svelte";
23
+
24
+ const AppLayer = Layer.mergeAll(
25
+ // your service layers
26
+ );
27
+
28
+ const RuntimeServer = ManagedRuntime.make(AppLayer);
29
+
30
+ export const remoteRunner = createRunner({
31
+ runtime: RuntimeServer,
32
+ before: () => Effect.log("Starting operation"),
33
+ after: () => Effect.log("Operation completed"),
34
+ onError: (err, isUnexpectedError) =>
35
+ isUnexpectedError
36
+ ? Effect.logError("Operation failed", err)
37
+ : Effect.void,
38
+ });
39
+ ```
40
+
41
+ ### 2. Use in Load Functions
42
+
43
+ The runner works directly inside SvelteKit `load` functions. Call it with an operation name, your Effect program, and an optional error-mapping pipeline:
44
+
45
+ ```ts
46
+ // src/routes/posts/+page.server.ts
47
+ import { Effect } from "effect";
48
+ import { httpErrorEffect } from "effect-svelte";
49
+ import { remoteRunner } from "$lib/server/runtime";
50
+
51
+ export async function load({ params }) {
52
+ return remoteRunner(
53
+ "load-posts",
54
+ Effect.gen(function* () {
55
+ const posts = yield* fetchPosts();
56
+
57
+ if (posts.length === 0) {
58
+ yield* httpErrorEffect(404, "NOT_FOUND", "No posts found");
59
+ }
60
+
61
+ return { posts };
62
+ }),
63
+ );
64
+ }
65
+ ```
66
+
67
+ ```svelte
68
+ <!-- src/routes/posts/+page.svelte -->
69
+ <script lang="ts">
70
+ let { data } = $props();
71
+ </script>
72
+
73
+ <ul>
74
+ {#each data.posts as post (post.id)}
75
+ <li>{post.title}</li>
76
+ {/each}
77
+ </ul>
78
+ ```
79
+
80
+ ### 3. Use in Remote Functions
81
+
82
+ The same runner works with SvelteKit's experimental [remote functions](https://svelte.dev/docs/kit/remote-functions):
83
+
84
+ ```ts
85
+ // src/routes/posts.remote.ts
86
+ import { Effect } from "effect";
87
+ import { query } from "$app/server";
88
+ import { remoteRunner } from "$lib/server/runtime";
89
+
90
+ export const getPosts = query(() =>
91
+ remoteRunner(
92
+ "get-posts",
93
+ Effect.gen(function* () {
94
+ yield* Effect.logInfo("Fetching posts");
95
+ return { posts: [{ id: 1, title: "Hello" }] };
96
+ }),
97
+ ),
98
+ );
99
+ ```
100
+
101
+ ```svelte
102
+ <!-- src/routes/+page.svelte -->
103
+ <script lang="ts">
104
+ import { getPosts } from "./posts.remote.ts";
105
+ </script>
106
+
107
+ <svelte:boundary>
108
+ {#snippet pending()}
109
+ <p>loading...</p>
110
+ {/snippet}
111
+
112
+ {#snippet failed(error)}
113
+ <span>Error: {error}</span>
114
+ {/snippet}
115
+
116
+ {@const { posts } = await getPosts()}
117
+ <ul>
118
+ {#each posts as post (post.id)}
119
+ <li>{post.title}</li>
120
+ {/each}
121
+ </ul>
122
+ </svelte:boundary>
123
+ ```
124
+
125
+ ## Error Handling
126
+
127
+ The library provides three tagged error types that map to SvelteKit's control flow:
128
+
129
+ ```ts
130
+ import { Effect } from "effect";
131
+ import {
132
+ SvelteKitRedirect,
133
+ SvelteKitHttpError,
134
+ SvelteKitInvalidError,
135
+ redirectEffect,
136
+ httpErrorEffect,
137
+ invalidEffect,
138
+ } from "effect-svelte";
139
+
140
+ Effect.gen(function* () {
141
+ // Redirect (throws SvelteKit redirect())
142
+ yield* redirectEffect(303, "/login");
143
+ // or: yield* Effect.fail(SvelteKitRedirect.make(303, "/login"));
144
+
145
+ // HTTP error (throws SvelteKit error())
146
+ yield* httpErrorEffect(404, "NOT_FOUND", "Resource not found");
147
+ // or: yield* Effect.fail(SvelteKitHttpError.make(404, "NOT_FOUND", "Not found"));
148
+
149
+ // Form validation error (throws SvelteKit invalid())
150
+ yield* invalidEffect("email", "Invalid email format");
151
+ // or: yield* Effect.fail(SvelteKitInvalidError.make({ email: "Invalid" }));
152
+ });
153
+ ```
154
+
155
+ ### Pipeline for Error Mapping
156
+
157
+ Use the third argument of the runner to map domain errors to SvelteKit errors:
158
+
159
+ ```ts
160
+ export const listProjects = remoteRunner(
161
+ "listProjects",
162
+ Effect.gen(function* () {
163
+ const service = yield* ProjectService;
164
+ return yield* service.all();
165
+ }),
166
+ (effect) =>
167
+ effect.pipe(
168
+ Effect.catchTags({
169
+ ParseError: () =>
170
+ httpErrorEffect(500, "PARSE_ERROR", "Unexpected data from the server"),
171
+ ResponseError: () =>
172
+ httpErrorEffect(500, "GENERIC_ERROR", "Unexpected server response"),
173
+ }),
174
+ ),
175
+ );
176
+ ```
177
+
178
+ ## API Reference
179
+
180
+ ### `createRunner(options)`
181
+
182
+ Creates a runner function that executes Effect programs in a SvelteKit context.
183
+
184
+ **Options:**
185
+
186
+ | Option | Type | Description |
187
+ |--------|------|-------------|
188
+ | `runtime` | `ManagedRuntime<R, never>` | The Effect runtime to use |
189
+ | `before` | `() => Effect<void>` | Runs before the effect |
190
+ | `after` | `(result: A) => Effect<void>` | Runs after successful execution |
191
+ | `onError` | `(err, isUnexpectedError) => Effect<void>` | Error handler. `isUnexpectedError` is `true` for 500+ errors and unrecognized failures, `false` for redirects, validation errors, and <500 HTTP errors |
192
+
193
+ **Returns:** `Runner<R>` - a function with signature:
194
+
195
+ ```ts
196
+ (operationName: string, effect: Effect<A, E, R>, pipeline?: PipelineFn<R>) => Promise<A>
197
+ ```
198
+
199
+ **Execution order:** `before` → `effect` → `pipeline` → `onError` (on failure) → `after` (on success) → `withSpan(operationName)`
200
+
201
+ ### Error Types
202
+
203
+ | Class | Converts to | Factory |
204
+ |-------|-------------|---------|
205
+ | `SvelteKitRedirect` | `redirect(status, location)` | `SvelteKitRedirect.make(status, location)` |
206
+ | `SvelteKitHttpError` | `error(status, body)` | `SvelteKitHttpError.make(status, code, message, details?)` |
207
+ | `SvelteKitInvalidError` | `invalid(issues)` | `SvelteKitInvalidError.make(issues)` |
208
+
209
+ ### Convenience Functions
210
+
211
+ | Function | Description |
212
+ |----------|-------------|
213
+ | `redirectEffect(status, location)` | Returns `Effect.fail(SvelteKitRedirect.make(...))` |
214
+ | `httpErrorEffect(status, code, message, details?)` | Returns `Effect.fail(SvelteKitHttpError.make(...))` |
215
+ | `invalidEffect(issues)` | Returns `Effect.fail(SvelteKitInvalidError.make(...))` |
216
+
217
+ ### `SvelteEffect.Code`
218
+
219
+ Error code type: `"GENERIC_ERROR" | "PARSE_ERROR" | "NOT_FOUND" | "UNAUTHORIZED"`
220
+
221
+ ## License
222
+
223
+ MIT
@@ -0,0 +1,67 @@
1
+ import type { HttpError, invalid, Redirect } from "@sveltejs/kit";
2
+ import { Effect } from "effect";
3
+ export declare namespace SvelteEffect {
4
+ type Code = "GENERIC_ERROR" | "PARSE_ERROR" | "NOT_FOUND" | "UNAUTHORIZED";
5
+ interface ErrorBody {
6
+ code: Code;
7
+ details?: Record<string, unknown>;
8
+ message: string;
9
+ timestamp: string;
10
+ }
11
+ }
12
+ export declare class SvelteKitError extends Error implements SvelteEffect.ErrorBody {
13
+ code: SvelteEffect.Code;
14
+ details?: Record<string, unknown>;
15
+ timestamp: string;
16
+ constructor(message: string, code: SvelteEffect.Code, details?: Record<string, unknown>);
17
+ }
18
+ declare const SvelteKitRedirect_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
19
+ readonly _tag: "SvelteKitRedirect";
20
+ } & Readonly<A>;
21
+ /**
22
+ * Tagged error representing a SvelteKit redirect.
23
+ *
24
+ * When handled by the runner it is converted to a thrown SvelteKit `redirect()`.
25
+ */
26
+ export declare class SvelteKitRedirect extends SvelteKitRedirect_base<{
27
+ readonly status: Redirect["status"];
28
+ readonly location: string;
29
+ }> {
30
+ static make(status: Redirect["status"], location: string): SvelteKitRedirect;
31
+ }
32
+ declare const SvelteKitHttpError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
33
+ readonly _tag: "SvelteKitHttpError";
34
+ } & Readonly<A>;
35
+ /**
36
+ * Tagged error representing a SvelteKit HTTP error.
37
+ *
38
+ * When handled by the runner it is converted to a thrown SvelteKit `error()`.
39
+ */
40
+ export declare class SvelteKitHttpError extends SvelteKitHttpError_base<{
41
+ readonly status: HttpError["status"];
42
+ readonly body: {
43
+ code: SvelteEffect.Code;
44
+ details?: Record<string, unknown> | undefined;
45
+ message: string;
46
+ timestamp: string;
47
+ };
48
+ }> {
49
+ static make(status: HttpError["status"], code: SvelteEffect.Code, message: string, details?: Record<string, unknown> | undefined): SvelteKitHttpError;
50
+ }
51
+ declare const SvelteKitInvalidError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
52
+ readonly _tag: "SvelteKitInvalidError";
53
+ } & Readonly<A>;
54
+ /**
55
+ * Tagged error representing SvelteKit form validation errors.
56
+ *
57
+ * When handled by the runner it is converted to a thrown SvelteKit `invalid()`.
58
+ */
59
+ export declare class SvelteKitInvalidError extends SvelteKitInvalidError_base<{
60
+ readonly issues: Parameters<typeof invalid>;
61
+ }> {
62
+ static make(...issues: Parameters<typeof invalid>): SvelteKitInvalidError;
63
+ }
64
+ export declare function redirectEffect(status: Redirect["status"], location: string): Effect.Effect<never, SvelteKitRedirect>;
65
+ export declare function httpErrorEffect(status: HttpError["status"], code: SvelteEffect.Code, message: string, details?: Record<string, unknown>): Effect.Effect<never, SvelteKitHttpError>;
66
+ export declare function invalidEffect(...issues: [field: string, message: string] | [Record<string, string>]): Effect.Effect<never, SvelteKitInvalidError>;
67
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,63 @@
1
+ import { Data, Effect } from "effect";
2
+ export class SvelteKitError extends Error {
3
+ constructor(message, code, details) {
4
+ super(message);
5
+ this.name = "SvelteKitError";
6
+ this.code = code;
7
+ this.details = details;
8
+ this.timestamp = new Date().toISOString();
9
+ }
10
+ }
11
+ // ---------------------------------------------------------------------------
12
+ // Tagged errors (used inside Effect programs)
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Tagged error representing a SvelteKit redirect.
16
+ *
17
+ * When handled by the runner it is converted to a thrown SvelteKit `redirect()`.
18
+ */
19
+ export class SvelteKitRedirect extends Data.TaggedError("SvelteKitRedirect") {
20
+ static make(status, location) {
21
+ return new SvelteKitRedirect({ location, status });
22
+ }
23
+ }
24
+ /**
25
+ * Tagged error representing a SvelteKit HTTP error.
26
+ *
27
+ * When handled by the runner it is converted to a thrown SvelteKit `error()`.
28
+ */
29
+ export class SvelteKitHttpError extends Data.TaggedError("SvelteKitHttpError") {
30
+ static make(status, code, message, details) {
31
+ return new SvelteKitHttpError({
32
+ body: {
33
+ code,
34
+ details,
35
+ message,
36
+ timestamp: new Date().toISOString(),
37
+ },
38
+ status,
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * Tagged error representing SvelteKit form validation errors.
44
+ *
45
+ * When handled by the runner it is converted to a thrown SvelteKit `invalid()`.
46
+ */
47
+ export class SvelteKitInvalidError extends Data.TaggedError("SvelteKitInvalidError") {
48
+ static make(...issues) {
49
+ return new SvelteKitInvalidError({ issues });
50
+ }
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Convenience effect constructors
54
+ // ---------------------------------------------------------------------------
55
+ export function redirectEffect(status, location) {
56
+ return Effect.fail(SvelteKitRedirect.make(status, location));
57
+ }
58
+ export function httpErrorEffect(status, code, message, details) {
59
+ return Effect.fail(SvelteKitHttpError.make(status, code, message, details));
60
+ }
61
+ export function invalidEffect(...issues) {
62
+ return Effect.fail(SvelteKitInvalidError.make(...issues));
63
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./errors.js";
2
+ export * from "./runner.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./errors.js";
2
+ export * from "./runner.js";
@@ -0,0 +1,10 @@
1
+ import { Effect, type ManagedRuntime } from "effect";
2
+ export interface RunnerOptions<R> {
3
+ runtime: ManagedRuntime.ManagedRuntime<R, never>;
4
+ before?: () => Effect.Effect<void>;
5
+ after?: <A>(result: A) => Effect.Effect<void>;
6
+ onError?: (err: unknown, isUnexpectedError: boolean) => Effect.Effect<void>;
7
+ }
8
+ export type PipelineFn<A, E, R> = (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
9
+ export type Runner<R> = <A, E>(operationName: string, effect: Effect.Effect<A, E, R>, pipeline?: PipelineFn<A, E, R>) => Promise<A>;
10
+ export declare function createRunner<R>(options: RunnerOptions<R>): Runner<R>;
package/dist/runner.js ADDED
@@ -0,0 +1,59 @@
1
+ import { error, invalid, redirect } from "@sveltejs/kit";
2
+ import { Cause, Effect, Exit, Function, Match, Option, } from "effect";
3
+ import { SvelteKitError, SvelteKitHttpError, SvelteKitInvalidError, SvelteKitRedirect, } from "./errors.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Internal helpers
6
+ // ---------------------------------------------------------------------------
7
+ function isUnexpectedError(err) {
8
+ if (err instanceof SvelteKitRedirect)
9
+ return false;
10
+ if (err instanceof SvelteKitInvalidError)
11
+ return false;
12
+ if (err instanceof SvelteKitHttpError && err.status < 500)
13
+ return false;
14
+ return true;
15
+ }
16
+ function handleExit(exit) {
17
+ return Exit.match(exit, {
18
+ onFailure: (cause) => Function.pipe(Cause.failureOption(cause), Option.match({
19
+ onNone: () => {
20
+ console.error("Unhandled Effect error:", Cause.pretty(cause));
21
+ throw error(500, new SvelteKitError("Internal Server Error", "GENERIC_ERROR"));
22
+ },
23
+ onSome: (failure) => Match.value(failure).pipe(Match.when(Match.instanceOf(SvelteKitRedirect), (err) => {
24
+ throw redirect(err.status, err.location);
25
+ }), Match.when(Match.instanceOf(SvelteKitHttpError), (err) => {
26
+ throw error(err.status, new SvelteKitError(err.body.message, err.body.code, err.body.details));
27
+ }), Match.when(Match.instanceOf(SvelteKitInvalidError), (err) => {
28
+ throw invalid(...err.issues);
29
+ }), Match.orElse(() => {
30
+ console.error("Unhandled Effect error:", Cause.pretty(cause));
31
+ throw error(500, new SvelteKitError("Internal Server Error", "GENERIC_ERROR"));
32
+ })),
33
+ })),
34
+ onSuccess: Function.identity,
35
+ });
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Public API
39
+ // ---------------------------------------------------------------------------
40
+ export function createRunner(options) {
41
+ return async function remoteRunner(operationName, effect, pipeline) {
42
+ let program = options.before
43
+ ? Effect.zipRight(options.before(), effect)
44
+ : effect;
45
+ if (pipeline) {
46
+ program = pipeline(program);
47
+ }
48
+ program = program.pipe(Effect.tapError((err) => {
49
+ if (!options.onError)
50
+ return Effect.void;
51
+ return options.onError(err, isUnexpectedError(err));
52
+ }));
53
+ if (options.after) {
54
+ program = program.pipe(Effect.tap(options.after));
55
+ }
56
+ const exit = await options.runtime.runPromiseExit(program.pipe(Effect.withSpan(operationName)));
57
+ return handleExit(exit);
58
+ };
59
+ }
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "author": {
3
+ "email": "daniel.kuroski@gmail.com",
4
+ "name": "Daniel Kuroski",
5
+ "url": "https://github.com/kuroski"
6
+ },
7
+ "bugs": {
8
+ "url": "https://github.com/kuroski/effect-svelte/issues"
9
+ },
10
+ "description": "Integration library for Effect with SvelteKit - seamless effect handling in SvelteKit remote functions",
11
+ "devDependencies": {
12
+ "@biomejs/biome": "2.3.13",
13
+ "@commitlint/cli": "20.4.0",
14
+ "@commitlint/config-conventional": "20.4.0",
15
+ "@semantic-release/changelog": "6.0.3",
16
+ "@semantic-release/git": "10.0.1",
17
+ "@sveltejs/kit": "2.50.1",
18
+ "@sveltejs/package": "2.5.7",
19
+ "@sveltejs/vite-plugin-svelte": "6.2.4",
20
+ "husky": "9.1.7",
21
+ "publint": "0.3.17",
22
+ "semantic-release": "25.0.3",
23
+ "svelte": "5.49.1",
24
+ "svelte-check": "4.3.6",
25
+ "typescript": "5.9.3",
26
+ "vite": "7.3.1",
27
+ "vitest": "4.0.18"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "!dist/**/*.test.*",
38
+ "!dist/**/*.spec.*"
39
+ ],
40
+ "homepage": "https://github.com/kuroski/effect-svelte#readme",
41
+ "keywords": [
42
+ "sveltekit",
43
+ "effect",
44
+ "effect-ts",
45
+ "svelte",
46
+ "functional",
47
+ "error-handling"
48
+ ],
49
+ "license": "MIT",
50
+ "name": "@kuroski/effect-svelte",
51
+ "peerDependencies": {
52
+ "@sveltejs/kit": ">=2.0.0",
53
+ "effect": ">=3.19.15 <4.0.0",
54
+ "svelte": ">=5.49.1"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/kuroski/effect-svelte.git"
59
+ },
60
+ "scripts": {
61
+ "biome:check": "biome check --write",
62
+ "biome:ci": "biome check",
63
+ "build": "vite build && bun run prepack",
64
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
65
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
66
+ "dev": "vite dev",
67
+ "prepack": "svelte-kit sync && svelte-package && publint",
68
+ "prepare": "husky && (svelte-kit sync || echo '')",
69
+ "preview": "vite preview",
70
+ "test": "vitest run",
71
+ "test:watch": "vitest"
72
+ },
73
+ "type": "module",
74
+ "types": "./dist/index.d.ts",
75
+ "version": "1.0.0",
76
+ "publishConfig": {
77
+ "access": "public",
78
+ "provenance": true
79
+ }
80
+ }