@thomasfosterau/effect-sveltekit 0.4.0 → 0.4.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.
package/README.md CHANGED
@@ -1,50 +1,60 @@
1
1
  # @thomasfosterau/effect-sveltekit
2
2
 
3
- Effect v4-native integration for **SvelteKit**: write `load` functions, form
4
- actions, endpoints, hooks, and remote functions as Effects; move custom types
5
- across the server/client boundary with Effect Schema; and deploy on an Effect
6
- HTTP server.
3
+ Effect integration for SvelteKit: load/action/endpoint wrappers, remote
4
+ functions, an `effectKit()` Vite plugin, and an Effect HTTP deployment adapter.
7
5
 
8
- Part of the [`effect-svelte`](https://github.com/thomasfosterau/effect-svelte)
9
- monorepo. Depends on
6
+ It depends on
10
7
  [`@thomasfosterau/effect-svelte`](https://www.npmjs.com/package/@thomasfosterau/effect-svelte)
11
8
  for the Svelte runes hooks.
12
9
 
13
- ## Installation
10
+ ## Install
14
11
 
15
12
  ```bash
16
- npm install @thomasfosterau/effect-sveltekit effect@rc svelte @sveltejs/kit
13
+ pnpm add @thomasfosterau/effect-sveltekit effect@rc svelte @sveltejs/kit
17
14
  # the Effect deployment adapter additionally needs:
18
- npm install @effect/platform-node@rc
15
+ pnpm add @effect/platform-node@rc
19
16
  ```
20
17
 
21
- ## Entry points
18
+ `effect` (`>=4.0.0-rc.118`) and `svelte` (`^5.40.0`) are required peers;
19
+ `@sveltejs/kit` (`^2.0.0 || ^3.0.0-next.0`), `vite` (for `./vite`) and
20
+ `@effect/platform-node` (`>=4.0.0-rc.118`, for `./adapter`) are optional. The
21
+ HTTP modules that lived in `@effect/platform` in v3 are part of `effect` itself
22
+ in v4 (`effect/http`), so no other Effect package is needed. Keep a single copy
23
+ of `effect`, and keep `effect` and `@effect/platform-node` on the same
24
+ exact version.
22
25
 
23
- | Import | What it provides |
24
- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
- | `@thomasfosterau/effect-sveltekit` | universal helpers (`load`, the `Remote` call namespace, `transport`, `sveltekitConfig`, `reroute`) |
26
- | `…/server` | server wrappers (`serverLoad`, `action`, `actions`, `handle`, `endpoint`), app runtimes (`makeServerRuntime` / `makeUniversalRuntime`), the per-request runtime seam (`makeRuntimeAdapter` / `RequestRuntime`), `RemoteFunction` |
27
- | `…/client` | browser-only `Navigation` / `State` / `Remote` namespaces and `useRemoteQuery` |
28
- | `…/vite` | the `effectKit()` Vite plugin (auto-wrap bare-Effect route exports) |
29
- | `…/adapter` | the Effect HTTP deployment adapter |
26
+ ## Usage
30
27
 
31
- ## Example
28
+ Wrap a route's `load` so it returns an Effect:
32
29
 
33
30
  ```ts
34
- // vite.config.ts — return Effects directly from route files
35
- import { sveltekit } from "@sveltejs/kit/vite";
36
- import { effectKit } from "@thomasfosterau/effect-sveltekit/vite";
31
+ // src/routes/items/[id]/+page.server.ts
32
+ import { serverLoad } from "@thomasfosterau/effect-sveltekit/server";
33
+ import { Effect } from "effect";
37
34
 
38
- export default { plugins: [effectKit(), sveltekit()] };
35
+ export const load = serverLoad((event) => Effect.succeed({ id: event.params.id }));
39
36
  ```
40
37
 
41
- ```ts
42
- // +page.server.ts
43
- import { Effect } from "effect";
44
- export const load = (event) => Effect.succeed({ id: event.params.id });
45
- ```
38
+ The same wrappers exist for actions, endpoints and hooks, on the default
39
+ per-request runtime or on your own services (see
40
+ [App-wide runtime](#app-wide-runtime-share-your-services-across-requests)); the
41
+ `effectKit()` Vite plugin lets route files export bare Effects instead.
42
+
43
+ ## API
44
+
45
+ | Entry point | What it provides |
46
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
+ | `@thomasfosterau/effect-sveltekit` | Universal helpers: `load` / `makeUniversalRuntime`, the `Remote` call namespace, `Hooks` (`reroute`, `transport`), `ResponseHeaders`, `sveltekitConfig` / `readConfig`, `apiClient` |
48
+ | `…/server` | Server wrappers (`serverLoad`, `action`, `actions`, `endpoint`), `Hooks`, `Endpoint`, `makeServerRuntime`, the per-request runtime seam (`makeRuntimeAdapter` / `RequestRuntime`), `Errors`, `RemoteFunction`, request decoding (`decodeParams`, `decodeJson`, …) |
49
+ | `…/client` | Browser-only `Navigation`, `State`, `Remote`, `Realtime`, `Forms` and `Hooks` namespaces, and `useRemoteQuery` |
50
+ | `…/vite` | The `effectKit()` Vite plugin (auto-wraps bare-Effect route exports) |
51
+ | `…/adapter` | The Effect HTTP deployment adapter (`effectAdapter`) |
52
+ | `…/adapter/handler` | The SvelteKit app as an Effect HTTP app (`makeHandler`, `makeHandlerWith`, `AssetSource`, `toWebHandler`) |
53
+ | `…/adapter/serve` | The Node server the adapter's `index.js` runs (`serve`, `layer`) |
54
+ | `…/testing` | A fake `RequestEvent` and runners for unit-testing server wrappers and remote functions (`TestRequestEvent`, `runServerLoad`, `runAction`, `runEndpoint`, `runRemote`) |
55
+ | `…/internal/auto` | Referenced by `effectKit()`'s generated code; not a public API |
46
56
 
47
- ## What's included
57
+ ### At a glance
48
58
 
49
59
  - **Effects as route exports** — explicit wrappers, app-wide runtimes (services
50
60
  built once per process), or the `effectKit()` plugin.
@@ -81,8 +91,477 @@ export const load = (event) => Effect.succeed({ id: event.params.id });
81
91
  (`AssetSource.fileSystem` for Node, `AssetSource.cloudflareAssets(env.ASSETS)`
82
92
  for Workers, or any custom source — R2, KV, a CDN passthrough).
83
93
 
84
- See the [monorepo README](https://github.com/thomasfosterau/effect-svelte#readme)
85
- for the full API and a runnable SvelteKit example.
94
+ ### SvelteKit Vite plugin (recommended)
95
+
96
+ ```ts
97
+ // vite.config.ts
98
+ import { sveltekit } from "@sveltejs/kit/vite";
99
+ import { effectKit } from "@thomasfosterau/effect-sveltekit/vite";
100
+
101
+ export default {
102
+ plugins: [effectKit(), sveltekit()],
103
+ };
104
+ ```
105
+
106
+ With the plugin installed, you can return an Effect (or a function returning
107
+ an Effect) directly from any standard SvelteKit route file (`+page.ts`,
108
+ `+page.server.ts`, `+layout(.server).ts`, `+server.ts`,
109
+ `hooks.(server|client).ts`):
110
+
111
+ ```ts
112
+ // +page.server.ts
113
+ import { Effect } from "effect";
114
+
115
+ export const load = (event) => Effect.succeed({ id: event.params.id });
116
+ ```
117
+
118
+ The plugin rewrites the export so it goes through the same runtime as the
119
+ explicit wrappers below; plain promise/value-returning functions pass through
120
+ unchanged.
121
+
122
+ #### Supported exports
123
+
124
+ | File | Exports auto-wrapped |
125
+ | ------------------- | ---------------------------------------------------------------------- |
126
+ | `+page.ts` | `load` |
127
+ | `+page.server.ts` | `load`, `actions` |
128
+ | `+layout.ts` | `load` |
129
+ | `+layout.server.ts` | `load` |
130
+ | `+server.ts` | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, `HEAD`, `fallback` |
131
+ | `hooks.ts` | `reroute` |
132
+ | `hooks.server.ts` | `handle`, `handleFetch`, `handleError`, `init` |
133
+ | `hooks.client.ts` | `handleError`, `init` |
134
+
135
+ Config exports like `prerender`, `ssr`, `csr`, and `trailingSlash` — and the
136
+ universal `transport` hook (which is plain data, not a function) — are never
137
+ touched.
138
+
139
+ ### App-wide runtime (share your services across requests)
140
+
141
+ Define your application's Effect services once as a `Layer`; the layer is
142
+ built lazily on the first request and shared for the lifetime of the server
143
+ process:
144
+
145
+ ```ts
146
+ // src/lib/server/runtime.ts
147
+ import { makeServerRuntime } from "@thomasfosterau/effect-sveltekit/server";
148
+ import { Layer } from "effect";
149
+ import { Database } from "./database";
150
+
151
+ export const runtime = makeServerRuntime(Database.layer);
152
+ ```
153
+
154
+ ```ts
155
+ // src/routes/items/+page.server.ts
156
+ import { runtime } from "$lib/server/runtime";
157
+ import { Effect } from "effect";
158
+ import { Database } from "$lib/server/database";
159
+
160
+ export const load = runtime.serverLoad((event) =>
161
+ Effect.gen(function* () {
162
+ const db = yield* Database;
163
+ return { items: yield* db.listItems() };
164
+ }),
165
+ );
166
+ ```
167
+
168
+ The runtime exposes `serverLoad`, `action`, `actions`, `handle`, `handleFetch`,
169
+ `handleError`, `init`, `endpoint`, `requestRuntime`, `runPromise`, and `dispose`.
170
+ For universal `load` functions there is a browser-safe counterpart,
171
+ `makeUniversalRuntime`, in `@thomasfosterau/effect-sveltekit`.
172
+
173
+ To make the same services available to bare-Effect exports handled by the
174
+ Vite plugin, point the plugin at the runtime module:
175
+
176
+ ```ts
177
+ // vite.config.ts
178
+ effectKit({ serverRuntime: "$lib/server/runtime" });
179
+ ```
180
+
181
+ #### One runtime per request
182
+
183
+ Wire the runtime's `handle` hook (or the auto-wrapped `handle` from the Vite
184
+ plugin) in `src/hooks.server.ts`. It creates a **per-request runtime** — the
185
+ application layer's services plus the request's `HttpClient`/`SvelteKitRequestEvent`,
186
+ built once and sharing a single request scope — and stores it on `event.locals`:
187
+
188
+ ```ts
189
+ // src/hooks.server.ts
190
+ import { runtime } from "$lib/server/runtime";
191
+ import { Effect } from "effect";
192
+
193
+ export const handle = runtime.handle(({ event, resolve }) =>
194
+ Effect.promise(async () => resolve(event)),
195
+ );
196
+ ```
197
+
198
+ With `handle` wired, every other wrapper for that runtime (`serverLoad`,
199
+ `action`, `endpoint`, ...) runs each request's effects on the same per-request
200
+ runtime — one `HttpClient`, one request scope — instead of rebuilding the request
201
+ services per call. Reach the runtime yourself with `runtime.requestRuntime(event)`
202
+ (for example from another hook in a `sequence`, to populate `event.locals`):
203
+
204
+ ```ts
205
+ // e.g. authenticate once per request
206
+ export const handle = runtime.handle(({ event, resolve }) =>
207
+ Effect.gen(function* () {
208
+ const rt = runtime.requestRuntime(event)!;
209
+ event.locals.user = yield* Effect.promise(() => rt.runPromise(Auth.currentUser));
210
+ return yield* Effect.promise(async () => resolve(event));
211
+ }),
212
+ );
213
+ ```
214
+
215
+ Wrappers from a _different_ runtime, or requests without `handle` wired, fall
216
+ back to building the request services per call, so this is backward compatible.
217
+
218
+ ### Effect deployment adapter
219
+
220
+ A SvelteKit [adapter](https://svelte.dev/docs/kit/adapters) (like
221
+ `@sveltejs/adapter-node`) whose production server is an Effect HTTP server
222
+ (`effect/http` + `@effect/platform-node`) — `Config`-based
223
+ configuration, request tracing spans, and graceful shutdown through Effect
224
+ scopes:
225
+
226
+ ```js
227
+ // svelte.config.js
228
+ import adapter from "@thomasfosterau/effect-sveltekit/adapter";
229
+
230
+ export default {
231
+ kit: { adapter: adapter() },
232
+ };
233
+ ```
234
+
235
+ ```bash
236
+ vite build
237
+ node build # PORT, HOST, ORIGIN, PROTOCOL_HEADER, ... read from the env
238
+ ```
239
+
240
+ The build also emits `build/handler.js`, which exports the whole SvelteKit
241
+ app as a composable Effect HTTP app — mount it inside an existing
242
+ `effect/http` server alongside your other routes:
243
+
244
+ ```ts
245
+ import { handler } from "./build/handler.js";
246
+ import { HttpServer } from "effect/http";
247
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
248
+ import { Effect, Layer } from "effect";
249
+ import { createServer } from "node:http";
250
+
251
+ const server = Layer.unwrap(Effect.map(handler, (app) => HttpServer.serve(app))).pipe(
252
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
253
+ );
254
+
255
+ NodeRuntime.runMain(Layer.launch(server));
256
+ ```
257
+
258
+ Unlike `adapter-node`, the server output is not bundled: deploy `build/`
259
+ together with `package.json` and production `node_modules` (which must
260
+ include `@thomasfosterau/effect-sveltekit`, `effect`, and
261
+ `@effect/platform-node`).
262
+
263
+ ### SvelteKit universal load
264
+
265
+ ```ts
266
+ import { load, makeUniversalRuntime } from "@thomasfosterau/effect-sveltekit";
267
+ ```
268
+
269
+ ### SvelteKit server helpers
270
+
271
+ ```ts
272
+ import {
273
+ serverLoad,
274
+ action,
275
+ actions,
276
+ endpoint,
277
+ // hooks.server.ts hooks as Effects: Hooks.handle / handleFetch / handleError / init
278
+ Hooks,
279
+ // create a +server.ts endpoint from an Effect HttpApi / HttpRouter / route
280
+ Endpoint,
281
+ makeServerRuntime,
282
+ } from "@thomasfosterau/effect-sveltekit/server";
283
+ ```
284
+
285
+ ### Boundary error translation from a catalogue (`Errors.toSvelteKit`)
286
+
287
+ Every server wrapper takes a `translateError` that turns a failed `Cause` into
288
+ SvelteKit control flow. Rather than hand-writing that switch, pass your tagged
289
+ error catalogue as data — keyed by `_tag` — and the app's `kit.ts` becomes one
290
+ call:
291
+
292
+ ```ts
293
+ // src/lib/server/effect/kit.ts
294
+ import {
295
+ Errors,
296
+ makeRuntimeAdapter,
297
+ RequestRuntime,
298
+ } from "@thomasfosterau/effect-sveltekit/server";
299
+ import { error } from "@sveltejs/kit";
300
+ import type { AppError } from "$lib/errors"; // NotFound | Unauthorized | Forbidden | Redirect | HttpError | JsonapiError
301
+
302
+ export const { serverLoad, endpoint } = makeRuntimeAdapter(
303
+ RequestRuntime.fromLocals((event) => event.locals.runtime),
304
+ {
305
+ translateError: Errors.toSvelteKit<AppError>({
306
+ NotFound: { status: 404, message: (e) => `${e.resource} ${e.id} not found` },
307
+ Unauthorized: 401, // body: the error's own `message`, else its tag
308
+ Forbidden: 403,
309
+ Redirect: "redirect", // redirect(e.status, e.to)
310
+ HttpError: "status", // error(e.status, e.message)
311
+ JsonapiError: (e) => error(e.status, JSON.stringify(e.toDocument())), // full control
312
+ }),
313
+ },
314
+ );
315
+ ```
316
+
317
+ The squashed cause is matched, so a tagged error that surfaced as a defect is
318
+ translated too. Anything outside the catalogue goes to `fallback` — by default
319
+ `SvelteKit.translate` (Kit control-flow errors pass through; everything else is
320
+ an `EffectCauseError` 500 carrying the full `Cause`); pass
321
+ `{ fallback: Errors.rethrowSquashed }` to rethrow the original value unchanged
322
+ for `handleError`. With the error union as the type argument, catalogue keys and
323
+ each entry's callback are checked against it.
324
+
325
+ ### SvelteKit endpoints backed by Effect
326
+
327
+ Create a `+server.ts` endpoint from an Effect [`HttpApi`](https://effect.website),
328
+ an `HttpRouter`, or a single route effect. Effect turns each of these into a Web
329
+ `fetch` handler; the `Endpoint` helpers wrap one as a SvelteKit `RequestHandler`.
330
+ Put it on a catch-all route and assign it to `fallback` so every method and
331
+ sub-path reaches it — `prefix` strips the SvelteKit mount path so the route's own
332
+ paths line up:
333
+
334
+ ```ts
335
+ // src/routes/api/[...path]/+server.ts
336
+ import { Endpoint } from "@thomasfosterau/effect-sveltekit/server";
337
+ import { api } from "$lib/server/api";
338
+ import { ApiLive } from "$lib/server/handlers"; // HttpApiBuilder.group(...) + your services
339
+
340
+ export const fallback = Endpoint.fromApi(api, ApiLive, { prefix: "/api" });
341
+ ```
342
+
343
+ `Endpoint.fromApi` supplies the platform services an `HttpApi` needs
344
+ (`HttpPlatform`, `FileSystem`, `Path`, `Etag`); `Endpoint.fromRouter` and
345
+ `Endpoint.fromHttpEffect` cover hand-built routers and single routes. The backing
346
+ layer is built once on the first request and shared; the returned handler carries
347
+ a `dispose()` for shutdown. Handlers receive an Effect-native `HttpServerRequest`
348
+ (built from `event.request`).
349
+
350
+ ### SvelteKit client helpers
351
+
352
+ Effect wrappers for `$app/navigation` (browser-only), plus the remote-function
353
+ call helpers:
354
+
355
+ ```ts
356
+ import {
357
+ // $app/navigation: promise wrappers (goto, invalidate, ...) and lifecycle
358
+ // events as Effect Streams (beforeNavigate/afterNavigate/onNavigate)
359
+ Navigation,
360
+ // $app/state: the current page as a Stream of snapshots
361
+ State,
362
+ // remote-function helpers (callQuery, callCommand, liveQueryStream, ...)
363
+ Remote,
364
+ // a reactive remote resource as an AsyncResult
365
+ useRemoteQuery,
366
+ // hooks.client.ts hooks as Effects: Hooks.handleError / Hooks.init
367
+ Hooks,
368
+ } from "@thomasfosterau/effect-sveltekit/client";
369
+
370
+ // e.g. Navigation.goto('/done'), Navigation.invalidateAll(),
371
+ // Remote.callQuery(getTodos), State.page()
372
+ ```
373
+
374
+ `Navigation.beforeNavigate()` / `Navigation.afterNavigate()` bridge
375
+ `$app/navigation`'s callback-registration functions to `Stream`s,
376
+ `Navigation.onNavigate(fn => Effect)` runs an Effect per navigation (forwarding
377
+ any cleanup the Effect yields), and `State.page()` emits a shallow snapshot of
378
+ `$app/state`'s `page` initially and after each navigation. Like the bare
379
+ SvelteKit functions they wrap, these must be called during component
380
+ initialization (at the top of `<script>`).
381
+
382
+ ### `transport` hook from Effect Schema
383
+
384
+ Move custom types (`Option`, `DateTime`, branded types, `Schema.Class`
385
+ instances, tagged errors, ...) across the server/client boundary — for `load`
386
+ data and remote functions — by building SvelteKit's universal `transport` hook
387
+ from Effect schemas (part of the universal `Hooks` namespace):
388
+
389
+ ```ts
390
+ // src/hooks.ts
391
+ import { Hooks } from "@thomasfosterau/effect-sveltekit";
392
+ import { Schema } from "effect";
393
+
394
+ export const transport = Hooks.transport({
395
+ Option: Schema.Option(Schema.Number),
396
+ Date: Schema.Date,
397
+ });
398
+ ```
399
+
400
+ ### Response-headers buffer (`ResponseHeaders`)
401
+
402
+ SvelteKit's `setHeaders` refuses `set-cookie`, throws when a header is set
403
+ twice, and is out of reach of components rendering during SSR. The usual answer
404
+ is a mutable headers buffer on `event.locals` that loads (and SSR components,
405
+ via `page.data`) append to and a `handle` copies onto the response.
406
+ `ResponseHeaders` is that buffer: a real `Headers` that serialises to `{}`, with
407
+ a transport entry that reconstructs it empty on the client (its contents never
408
+ leave the server):
409
+
410
+ ```ts
411
+ // src/app.d.ts
412
+ declare global {
413
+ namespace App {
414
+ interface Locals {
415
+ headers: import("@thomasfosterau/effect-sveltekit").ResponseHeaders;
416
+ }
417
+ }
418
+ }
419
+
420
+ // src/hooks.ts
421
+ import { Hooks } from "@thomasfosterau/effect-sveltekit";
422
+ export const transport = Hooks.transport({ ResponseHeaders: Hooks.responseHeadersTransporter });
423
+
424
+ // src/hooks.server.ts
425
+ import { ResponseHeaders } from "@thomasfosterau/effect-sveltekit";
426
+ import type { Handle } from "@sveltejs/kit";
427
+
428
+ export const handle: Handle = async ({ event, resolve }) => {
429
+ event.locals.headers = new ResponseHeaders();
430
+ const response = await resolve(event);
431
+ event.locals.headers.appendTo(response.headers); // appends, so every set-cookie survives
432
+ return response;
433
+ };
434
+
435
+ // src/routes/+layout.server.ts — expose the same buffer to every page
436
+ export const load = ({ locals }) => ({ headers: locals.headers });
437
+ ```
438
+
439
+ (`SvelteKitResponseHeaders`, the per-request _service_, wraps `setHeaders` for
440
+ handler effects; `ResponseHeaders` is the buffer for everything `setHeaders`
441
+ cannot do.)
442
+
443
+ ### `$env` as an Effect `Config` source
444
+
445
+ Back Effect's `Config` with SvelteKit's environment, so app-layer services read
446
+ configuration from `$env`:
447
+
448
+ ```ts
449
+ // src/lib/server/runtime.ts
450
+ import { env } from "$env/dynamic/private";
451
+ import { makeServerRuntime, sveltekitConfig } from "@thomasfosterau/effect-sveltekit/server";
452
+ import { Layer } from "effect";
453
+ import { Database } from "./database"; // its layer reads Config.String("DATABASE_URL")
454
+
455
+ export const runtime = makeServerRuntime(Database.layer.pipe(Layer.provide(sveltekitConfig(env))));
456
+ ```
457
+
458
+ ### SvelteKit hooks as Effects (`Hooks`)
459
+
460
+ Every SvelteKit hook has an explicit Effect wrapper, grouped into a `Hooks`
461
+ namespace per hook file (mirroring the auto-wrap table above):
462
+
463
+ | Import path | `Hooks` members | Runs on |
464
+ | ----------------------------------------- | ---------------------------------------------- | ------------------------------------------------ |
465
+ | `@thomasfosterau/effect-sveltekit` | `reroute`, `transport`, `transporter` | universal runtime (`HttpClient` via event fetch) |
466
+ | `@thomasfosterau/effect-sveltekit/server` | `handle`, `handleFetch`, `handleError`, `init` | server runtime (`HttpClient` + `RequestEvent`) |
467
+ | `@thomasfosterau/effect-sveltekit/client` | `handleError`, `init` | a bare Effect runtime |
468
+
469
+ ```ts
470
+ // src/hooks.ts
471
+ import { Hooks } from "@thomasfosterau/effect-sveltekit";
472
+ import { Effect } from "effect";
473
+
474
+ export const reroute = Hooks.reroute((event) =>
475
+ Effect.succeed(event.url.pathname === "/old" ? "/new" : undefined),
476
+ );
477
+ ```
478
+
479
+ ```ts
480
+ // src/hooks.server.ts
481
+ import { Hooks } from "@thomasfosterau/effect-sveltekit/server";
482
+ import { Effect } from "effect";
483
+
484
+ export const init = Hooks.init(Effect.logInfo("server starting up"));
485
+
486
+ export const handle = Hooks.handle(({ event, resolve }) =>
487
+ Effect.gen(function* () {
488
+ const start = Date.now();
489
+ const response = yield* Effect.promise(async () => resolve(event));
490
+ yield* Effect.logInfo(`${event.url.pathname} -> ${response.status} (${Date.now() - start}ms)`);
491
+ return response;
492
+ }),
493
+ );
494
+
495
+ export const handleError = Hooks.handleError(({ error, event }) =>
496
+ Effect.gen(function* () {
497
+ const id = crypto.randomUUID();
498
+ yield* Effect.logError(`${event.url.pathname} (${id})`, error);
499
+ return { message: "Internal Error", id };
500
+ }),
501
+ );
502
+ ```
503
+
504
+ ```ts
505
+ // src/hooks.client.ts
506
+ import { Hooks } from "@thomasfosterau/effect-sveltekit/client";
507
+ import { Effect } from "effect";
508
+
509
+ export const handleError = Hooks.handleError(({ error }) =>
510
+ Effect.as(Effect.logError("client navigation failed", error), {
511
+ message: "Something went wrong",
512
+ }),
513
+ );
514
+ ```
515
+
516
+ To additionally provide your application's services to the server hooks, use the
517
+ matching methods on an app-wide runtime (`runtime.handle`, `runtime.handleFetch`,
518
+ `runtime.handleError`, `runtime.init`). The same exports also work as bare
519
+ Effects (no explicit wrapper) when the `effectKit()` Vite plugin is enabled.
520
+
521
+ ### Native form actions
522
+
523
+ For SvelteKit page `actions`, the `Forms` namespace
524
+ (`@thomasfosterau/effect-sveltekit/client`) expresses progressive enhancement
525
+ with Effects: `Forms.enhance({ onSubmit, onResult })` builds a `SubmitFunction`
526
+ for `use:enhance`, and `Forms.applyAction` / `Forms.deserialize` wrap the
527
+ `$app/forms` helpers. (This complements `Remote.submitForm`, which covers remote
528
+ forms.)
529
+
530
+ ## Recipes
531
+
532
+ ### App-side patterns
533
+
534
+ Two SvelteKit app patterns are recipes rather than APIs:
535
+
536
+ - **Leaving an error page.** After an error page renders, client-side
537
+ navigation can re-run loads against a stale context; the apps force a full
538
+ reload instead:
539
+
540
+ ```svelte
541
+ <script lang="ts">
542
+ import { beforeNavigate } from "$app/navigation";
543
+ import { page } from "$app/state";
544
+
545
+ beforeNavigate((navigation) => {
546
+ if (page.error) {
547
+ navigation.cancel();
548
+ window.location.href = navigation.to?.url.href ?? "/";
549
+ }
550
+ });
551
+ </script>
552
+ ```
553
+
554
+ - **Paraglide.** The paraglide wiring (plugin shim, compile step, the
555
+ `paraglide` handle and the `fillPattern` workaround) is SvelteKit/Vite-specific
556
+ and not Effect, so it stays an app template rather than a `./paraglide` entry
557
+ here.
558
+
559
+ ### Cookbook
560
+
561
+ See the [cookbook](../../docs/cookbook.md) for recipes — consuming an
562
+ `Endpoint.fromApi` with a typed `apiClient`, reading build-time vs runtime
563
+ `$env` config with `readConfig` / `sveltekitConfig`, loading/error chrome around
564
+ an atom query, and a typeahead on `useDebouncedSearch`.
86
565
 
87
566
  ## License
88
567
 
package/dist/Remote.d.ts CHANGED
@@ -3,9 +3,36 @@ import { RemoteCommand, RemoteLiveQuery, RemotePrerenderFunction, RemoteQuery, R
3
3
  declare namespace Remote_d_exports {
4
4
  export { RemoteSubmit, callCommand, callCommandWithUpdates, callPrerender, callQuery, liveQueryStream, submitForm, submitFormWithUpdates };
5
5
  }
6
+ /**
7
+ * Calls a SvelteKit remote `query` as an Effect that succeeds with its output.
8
+ *
9
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
10
+ * defect, not on the error channel, despite the declared `unknown` error type.
11
+ */
6
12
  declare function callQuery<Output>(query: RemoteQueryFunction<void, Output>): Effect.Effect<Output, unknown, never>;
13
+ /**
14
+ * Calls a SvelteKit remote `query` with `input` as an Effect that succeeds
15
+ * with its output.
16
+ *
17
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
18
+ * defect, not on the error channel, despite the declared `unknown` error type.
19
+ */
7
20
  declare function callQuery<Input, Output>(query: RemoteQueryFunction<Input, Output>, input: Input): Effect.Effect<Output, unknown, never>;
21
+ /**
22
+ * Calls a SvelteKit remote `command` as an Effect that succeeds with its
23
+ * output.
24
+ *
25
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
26
+ * defect, not on the error channel, despite the declared `unknown` error type.
27
+ */
8
28
  declare function callCommand<Output>(command: RemoteCommand<void, Output>): Effect.Effect<Output, unknown, never>;
29
+ /**
30
+ * Calls a SvelteKit remote `command` with `input` as an Effect that succeeds
31
+ * with its output.
32
+ *
33
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
34
+ * defect, not on the error channel, despite the declared `unknown` error type.
35
+ */
9
36
  declare function callCommand<Input, Output>(command: RemoteCommand<Input, Output>, input: Input): Effect.Effect<Output, unknown, never>;
10
37
  /**
11
38
  * Calls a remote command and refreshes the given queries in the same round
@@ -14,9 +41,26 @@ declare function callCommand<Input, Output>(command: RemoteCommand<Input, Output
14
41
  * Newer SvelteKit versions cache remote query instances, so a command that
15
42
  * changes data those queries depend on must list them here (or call
16
43
  * `query.refresh()` afterwards) for their cached values to update.
44
+ *
45
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
46
+ * defect, not on the error channel, despite the declared `unknown` error type.
17
47
  */
18
48
  declare function callCommandWithUpdates<Input, Output>(command: RemoteCommand<Input, Output>, input: Input, ...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Effect.Effect<Output, unknown, never>;
49
+ /**
50
+ * Calls a SvelteKit remote `prerender` function as an Effect that succeeds
51
+ * with its output.
52
+ *
53
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
54
+ * defect, not on the error channel, despite the declared `unknown` error type.
55
+ */
19
56
  declare function callPrerender<Output>(prerender: RemotePrerenderFunction<void, Output>): Effect.Effect<Output, unknown, never>;
57
+ /**
58
+ * Calls a SvelteKit remote `prerender` function with `input` as an Effect
59
+ * that succeeds with its output.
60
+ *
61
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
62
+ * defect, not on the error channel, despite the declared `unknown` error type.
63
+ */
20
64
  declare function callPrerender<Input, Output>(prerender: RemotePrerenderFunction<Input, Output>, input: Input): Effect.Effect<Output, unknown, never>;
21
65
  /**
22
66
  * The shape of an enhanced remote form `submit` function. The promise result
@@ -26,7 +70,20 @@ declare function callPrerender<Input, Output>(prerender: RemotePrerenderFunction
26
70
  type RemoteSubmit = () => PromiseLike<unknown> & {
27
71
  updates: (...queries: Array<RemoteQuery<any> | RemoteQueryOverride>) => PromiseLike<unknown>;
28
72
  };
73
+ /**
74
+ * Runs an enhanced remote form's `submit()` as an Effect.
75
+ *
76
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
77
+ * defect, not on the error channel, despite the declared `unknown` error type.
78
+ */
29
79
  declare function submitForm(submit: () => PromiseLike<unknown>): Effect.Effect<void, unknown, never>;
80
+ /**
81
+ * Runs an enhanced remote form's `submit()` and refreshes the given queries in
82
+ * the same round trip (SvelteKit's single-flight mutations).
83
+ *
84
+ * A rejected call (including a thrown `error()` / `redirect()`) surfaces as a
85
+ * defect, not on the error channel, despite the declared `unknown` error type.
86
+ */
30
87
  declare function submitFormWithUpdates(submit: RemoteSubmit, ...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Effect.Effect<void, unknown, never>;
31
88
  /**
32
89
  * Consumes a SvelteKit live query (`query.live(...)`) as an Effect `Stream`.