@thomasfosterau/effect-sveltekit 0.1.1 → 0.2.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/dist/api-client.d.ts +32 -0
- package/dist/api-client.js +44 -0
- package/dist/api-client.js.map +1 -0
- package/dist/app-runtime.d.ts +2 -1
- package/dist/app-runtime.js +13 -11
- package/dist/app-runtime.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +3 -1
- package/dist/config.d.ts +33 -2
- package/dist/config.js +33 -2
- package/dist/config.js.map +1 -1
- package/dist/forms.d.ts +30 -0
- package/dist/forms.js +66 -0
- package/dist/forms.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/internal/enhance.d.ts +39 -0
- package/dist/internal/enhance.js +24 -0
- package/dist/internal/enhance.js.map +1 -0
- package/dist/internal/remote-run.d.ts +12 -1
- package/dist/internal/remote-run.js +3 -5
- package/dist/internal/remote-run.js.map +1 -1
- package/dist/internal/sse.js +37 -0
- package/dist/internal/sse.js.map +1 -0
- package/dist/kit.d.ts +130 -0
- package/dist/kit.js +185 -0
- package/dist/kit.js.map +1 -0
- package/dist/realtime.d.ts +105 -0
- package/dist/realtime.js +111 -0
- package/dist/realtime.js.map +1 -0
- package/dist/request-parse.d.ts +89 -0
- package/dist/request-parse.js +121 -0
- package/dist/request-parse.js.map +1 -0
- package/dist/request-runtime.js +17 -16
- package/dist/request-runtime.js.map +1 -1
- package/dist/request-services.d.ts +120 -0
- package/dist/request-services.js +120 -0
- package/dist/request-services.js.map +1 -0
- package/dist/server/Endpoint.d.ts +62 -2
- package/dist/server/Endpoint.js +47 -2
- package/dist/server/Endpoint.js.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.js +5 -1
- package/dist/server.js.map +1 -1
- package/dist/streamed-load.d.ts +23 -0
- package/dist/streamed-load.js +74 -0
- package/dist/streamed-load.js.map +1 -0
- package/dist/testing.d.ts +175 -0
- package/dist/testing.js +153 -0
- package/dist/testing.js.map +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enhance.js","names":[],"sources":["../../src/internal/enhance.ts"],"sourcesContent":["/**\n * The runtime-agnostic core of the `use:enhance` bridge, kept free of any\n * `$app/*` import so it can be unit-tested outside a SvelteKit app build.\n */\n\nimport type { SubmitFunction } from \"@sveltejs/kit\";\nimport { Context, Effect, ManagedRuntime } from \"effect\";\n\n/** The input SvelteKit passes to a `SubmitFunction` before the request is sent. */\nexport type SubmitInput = Parameters<SubmitFunction>[0];\n\n/** The result callback a `SubmitFunction` may return, run once the action responds. */\ntype ResultCallback = Extract<Awaited<ReturnType<SubmitFunction>>, (...args: never) => unknown>;\n\n/** The options passed to the result callback (`result`, `update`, ...). */\nexport type ResultOptions = Parameters<ResultCallback>[0];\n\n/** A runtime the handler effects can run against (a `Context` or `ManagedRuntime`). */\nexport type EnhanceRuntime<R> = Context.Context<R> | ManagedRuntime.ManagedRuntime<R, never>;\n\n/** Effect-based handlers for the two phases of an enhanced form submission. */\nexport interface EnhanceHandlers<R = never> {\n /**\n * Runs before the request is sent. Call `input.cancel()` to abort the\n * submission (the result phase then never runs). Use for optimistic UI or\n * validation.\n */\n readonly onSubmit?: (input: SubmitInput) => Effect.Effect<void, never, R>;\n /**\n * Runs once the action responds, with the `ActionResult` and SvelteKit's\n * `update()` (which applies the default behaviour — updating `page.form`,\n * resetting the form, invalidating). If omitted, SvelteKit's default\n * behaviour applies automatically.\n */\n readonly onResult?: (options: ResultOptions) => Effect.Effect<void, never, R>;\n}\n\n/** Options for {@link makeSubmitFunction}. */\nexport interface EnhanceOptions<R = never> {\n /**\n * The runtime to run the handler effects against. Omit when the handlers\n * require no services (`R = never`); they then run on the default runtime.\n */\n readonly runtime?: EnhanceRuntime<R>;\n}\n\n/** Run an `Effect<A, never, R>` on the given runtime (or the default when none). */\nconst runPromiseWith =\n <R>(runtime: EnhanceRuntime<R> | undefined) =>\n <A>(effect: Effect.Effect<A, never, R>): Promise<A> => {\n if (runtime === undefined) return Effect.runPromise(effect as Effect.Effect<A, never, never>);\n return Context.isContext(runtime)\n ? Effect.runPromiseWith(runtime)(effect)\n : runtime.runPromise(effect);\n };\n\n/**\n * Build a SvelteKit `SubmitFunction` from Effect-based handlers. See\n * `Forms.enhance` for the public entry point and documentation.\n */\nexport const makeSubmitFunction = <R = never>(\n handlers: EnhanceHandlers<R>,\n options?: EnhanceOptions<R>,\n): SubmitFunction => {\n const run = runPromiseWith(options?.runtime);\n return async (input) => {\n if (handlers.onSubmit) await run(handlers.onSubmit(input));\n // Returning `undefined` lets SvelteKit apply its default result behaviour.\n if (!handlers.onResult) return undefined;\n const onResult = handlers.onResult;\n return (resultOptions) => run(onResult(resultOptions));\n };\n};\n"],"mappings":";;;AA+CA,MAAM,kBACA,aACA,WAAmD;CACrD,IAAI,YAAY,KAAA,GAAW,OAAO,OAAO,WAAW,MAAwC;CAC5F,OAAO,QAAQ,UAAU,OAAO,IAC5B,OAAO,eAAe,OAAO,EAAE,MAAM,IACrC,QAAQ,WAAW,MAAM;AAC/B;;;;;AAMF,MAAa,sBACX,UACA,YACmB;CACnB,MAAM,MAAM,eAAe,SAAS,OAAO;CAC3C,OAAO,OAAO,UAAU;EACtB,IAAI,SAAS,UAAU,MAAM,IAAI,SAAS,SAAS,KAAK,CAAC;EAEzD,IAAI,CAAC,SAAS,UAAU,OAAO,KAAA;EAC/B,MAAM,WAAW,SAAS;EAC1B,QAAQ,kBAAkB,IAAI,SAAS,aAAa,CAAC;CACvD;AACF"}
|
|
@@ -12,6 +12,17 @@ type FormErrorMapper = (error: unknown, ctx: {
|
|
|
12
12
|
readonly issue: InvalidField<any>;
|
|
13
13
|
readonly data: unknown;
|
|
14
14
|
}) => never;
|
|
15
|
+
/** Runs a remote handler's effect and settles it (value or thrown via translator). */
|
|
16
|
+
type Runner = <A>(effect: Effect.Effect<A, unknown, any>) => Promise<A>;
|
|
17
|
+
/** Like {@link Runner}, but with the form's `issue` / `data` for error interop. */
|
|
18
|
+
type FormRunner = <A>(effect: Effect.Effect<A, unknown, any>, ctx: {
|
|
19
|
+
readonly issue: InvalidField<any>;
|
|
20
|
+
readonly data: unknown;
|
|
21
|
+
}) => Promise<A>;
|
|
22
|
+
interface RunHelpers {
|
|
23
|
+
readonly run: Runner;
|
|
24
|
+
readonly runForm: FormRunner;
|
|
25
|
+
}
|
|
15
26
|
//#endregion
|
|
16
|
-
export { FormErrorMapper };
|
|
27
|
+
export { FormErrorMapper, RunHelpers };
|
|
17
28
|
//# sourceMappingURL=remote-run.d.ts.map
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SvelteKit } from "../kit.js";
|
|
2
2
|
import { Cause, Exit } from "effect";
|
|
3
3
|
//#region src/internal/remote-run.ts
|
|
4
|
-
/** The default translator:
|
|
5
|
-
const defaultTranslate =
|
|
6
|
-
throw causeToThrowable(cause);
|
|
7
|
-
};
|
|
4
|
+
/** The default translator: Kit control-flow errors through, anything else a 500. */
|
|
5
|
+
const defaultTranslate = SvelteKit.translate;
|
|
8
6
|
/** Build the `run` / `runForm` helpers from an exit resolver and a translator. */
|
|
9
7
|
const makeRunHelpers = (resolve, translate, formError) => {
|
|
10
8
|
const run = (effect) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-run.js","names":[],"sources":["../../src/internal/remote-run.ts"],"sourcesContent":["/**\n * Run/settle helpers for the remote-function wrappers, factored out so they can\n * be unit-tested without importing `$app/server` (a SvelteKit virtual module\n * unavailable outside an app build). The `$app/server`-dependent resolvers live\n * in `remote.server.ts`; this module only deals in `Exit`s.\n */\n\nimport type { InvalidField } from \"@sveltejs/kit\";\nimport { Cause, type Effect, Exit } from \"effect\";\nimport {
|
|
1
|
+
{"version":3,"file":"remote-run.js","names":[],"sources":["../../src/internal/remote-run.ts"],"sourcesContent":["/**\n * Run/settle helpers for the remote-function wrappers, factored out so they can\n * be unit-tested without importing `$app/server` (a SvelteKit virtual module\n * unavailable outside an app build). The `$app/server`-dependent resolvers live\n * in `remote.server.ts`; this module only deals in `Exit`s.\n */\n\nimport type { InvalidField } from \"@sveltejs/kit\";\nimport { Cause, type Effect, Exit } from \"effect\";\nimport type { ErrorTranslator } from \"../request-runtime.js\";\nimport { SvelteKit } from \"../kit.js\";\n\n/** The default translator: Kit control-flow errors through, anything else a 500. */\nexport const defaultTranslate: ErrorTranslator = SvelteKit.translate;\n\n/**\n * Maps a remote *form* handler's failure into SvelteKit's field issues, given\n * the form's parsed `data` and the `issue` helper. It may call `invalid()` so a\n * structured/nested validation error drives field-level invalidation instead of\n * a generic failure. Receives the squashed handler failure.\n */\nexport type FormErrorMapper = (\n error: unknown,\n ctx: { readonly issue: InvalidField<any>; readonly data: unknown },\n) => never;\n\n/** Runs a remote handler's effect and settles it (value or thrown via translator). */\nexport type Runner = <A>(effect: Effect.Effect<A, unknown, any>) => Promise<A>;\n\n/** Like {@link Runner}, but with the form's `issue` / `data` for error interop. */\nexport type FormRunner = <A>(\n effect: Effect.Effect<A, unknown, any>,\n ctx: { readonly issue: InvalidField<any>; readonly data: unknown },\n) => Promise<A>;\n\nexport interface RunHelpers {\n readonly run: Runner;\n readonly runForm: FormRunner;\n}\n\n/** How to obtain an `Exit` for the current request, plus optional teardown. */\nexport type ExitResolver = () => {\n readonly runExit: <A>(effect: Effect.Effect<A, unknown, any>) => Promise<Exit.Exit<A, unknown>>;\n readonly dispose?: () => void;\n};\n\n/** Build the `run` / `runForm` helpers from an exit resolver and a translator. */\nexport const makeRunHelpers = (\n resolve: ExitResolver,\n translate: ErrorTranslator,\n formError?: FormErrorMapper,\n): RunHelpers => {\n const run: Runner = (effect) => {\n const { runExit, dispose } = resolve();\n return runExit(effect).then((exit) => {\n try {\n return Exit.isSuccess(exit) ? exit.value : translate(exit.cause);\n } finally {\n dispose?.();\n }\n });\n };\n const runForm: FormRunner = (effect, ctx) => {\n const { runExit, dispose } = resolve();\n return runExit(effect).then((exit) => {\n try {\n if (Exit.isSuccess(exit)) return exit.value;\n return formError ? formError(Cause.squash(exit.cause), ctx) : translate(exit.cause);\n } finally {\n dispose?.();\n }\n });\n };\n return { run, runForm };\n};\n"],"mappings":";;;;AAaA,MAAa,mBAAoC,UAAU;;AAkC3D,MAAa,kBACX,SACA,WACA,cACe;CACf,MAAM,OAAe,WAAW;EAC9B,MAAM,EAAE,SAAS,YAAY,QAAQ;EACrC,OAAO,QAAQ,MAAM,EAAE,MAAM,SAAS;GACpC,IAAI;IACF,OAAO,KAAK,UAAU,IAAI,IAAI,KAAK,QAAQ,UAAU,KAAK,KAAK;GACjE,UAAU;IACR,UAAU;GACZ;EACF,CAAC;CACH;CACA,MAAM,WAAuB,QAAQ,QAAQ;EAC3C,MAAM,EAAE,SAAS,YAAY,QAAQ;EACrC,OAAO,QAAQ,MAAM,EAAE,MAAM,SAAS;GACpC,IAAI;IACF,IAAI,KAAK,UAAU,IAAI,GAAG,OAAO,KAAK;IACtC,OAAO,YAAY,UAAU,MAAM,OAAO,KAAK,KAAK,GAAG,GAAG,IAAI,UAAU,KAAK,KAAK;GACpF,UAAU;IACR,UAAU;GACZ;EACF,CAAC;CACH;CACA,OAAO;EAAE;EAAK;CAAQ;AACxB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/internal/sse.ts
|
|
2
|
+
/**
|
|
3
|
+
* Server-Sent Events framing helpers, factored out of the endpoint wrapper so
|
|
4
|
+
* the wire format can be unit-tested in isolation.
|
|
5
|
+
*
|
|
6
|
+
* See the [SSE specification](https://html.spec.whatwg.org/multipage/server-sent-events.html).
|
|
7
|
+
*/
|
|
8
|
+
/** The response headers required for a `text/event-stream` body. */
|
|
9
|
+
const SSE_HEADERS = {
|
|
10
|
+
"Content-Type": "text/event-stream",
|
|
11
|
+
"Cache-Control": "no-cache, no-transform",
|
|
12
|
+
Connection: "keep-alive"
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Encode a single SSE event as its wire representation, terminated by the
|
|
16
|
+
* blank line that dispatches it.
|
|
17
|
+
*
|
|
18
|
+
* `data` is split on newlines into one `data:` line each (per the spec, a bare
|
|
19
|
+
* newline in a value would otherwise be ambiguous). `event` / `id` are emitted
|
|
20
|
+
* only when present.
|
|
21
|
+
*/
|
|
22
|
+
const encodeSseFrame = (frame) => {
|
|
23
|
+
let out = "";
|
|
24
|
+
if (frame.id !== void 0) out += `id: ${frame.id}\n`;
|
|
25
|
+
if (frame.event !== void 0) out += `event: ${frame.event}\n`;
|
|
26
|
+
for (const line of frame.data.split("\n")) out += `data: ${line}\n`;
|
|
27
|
+
out += "\n";
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
/** Encode the one-shot `retry:` field that sets the client reconnection delay. */
|
|
31
|
+
const encodeRetry = (milliseconds) => `retry: ${milliseconds}\n\n`;
|
|
32
|
+
/** A comment line used as a heartbeat/keepalive (ignored by the client). */
|
|
33
|
+
const HEARTBEAT_FRAME = ": heartbeat\n\n";
|
|
34
|
+
//#endregion
|
|
35
|
+
export { HEARTBEAT_FRAME, SSE_HEADERS, encodeRetry, encodeSseFrame };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","names":[],"sources":["../../src/internal/sse.ts"],"sourcesContent":["/**\n * Server-Sent Events framing helpers, factored out of the endpoint wrapper so\n * the wire format can be unit-tested in isolation.\n *\n * See the [SSE specification](https://html.spec.whatwg.org/multipage/server-sent-events.html).\n */\n\n/** The response headers required for a `text/event-stream` body. */\nexport const SSE_HEADERS: Readonly<Record<string, string>> = {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n};\n\n/** The fields of a single SSE event. */\nexport interface SseFrame {\n /** The event payload. Multi-line strings are split into multiple `data:` lines. */\n readonly data: string;\n /** Optional event name (`event:` field). */\n readonly event?: string | undefined;\n /** Optional event id (`id:` field), which the browser echoes on reconnect. */\n readonly id?: string | undefined;\n}\n\n/**\n * Encode a single SSE event as its wire representation, terminated by the\n * blank line that dispatches it.\n *\n * `data` is split on newlines into one `data:` line each (per the spec, a bare\n * newline in a value would otherwise be ambiguous). `event` / `id` are emitted\n * only when present.\n */\nexport const encodeSseFrame = (frame: SseFrame): string => {\n let out = \"\";\n if (frame.id !== undefined) out += `id: ${frame.id}\\n`;\n if (frame.event !== undefined) out += `event: ${frame.event}\\n`;\n // Split into per-line `data:` fields so embedded newlines survive.\n for (const line of frame.data.split(\"\\n\")) out += `data: ${line}\\n`;\n out += \"\\n\";\n return out;\n};\n\n/** Encode the one-shot `retry:` field that sets the client reconnection delay. */\nexport const encodeRetry = (milliseconds: number): string => `retry: ${milliseconds}\\n\\n`;\n\n/** A comment line used as a heartbeat/keepalive (ignored by the client). */\nexport const HEARTBEAT_FRAME = \": heartbeat\\n\\n\";\n"],"mappings":";;;;;;;;AAQA,MAAa,cAAgD;CAC3D,gBAAgB;CAChB,iBAAiB;CACjB,YAAY;AACd;;;;;;;;;AAoBA,MAAa,kBAAkB,UAA4B;CACzD,IAAI,MAAM;CACV,IAAI,MAAM,OAAO,KAAA,GAAW,OAAO,OAAO,MAAM,GAAG;CACnD,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,UAAU,MAAM,MAAM;CAE5D,KAAK,MAAM,QAAQ,MAAM,KAAK,MAAM,IAAI,GAAG,OAAO,SAAS,KAAK;CAChE,OAAO;CACP,OAAO;AACT;;AAGA,MAAa,eAAe,iBAAiC,UAAU,aAAa;;AAGpF,MAAa,kBAAkB"}
|
package/dist/kit.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Cause, Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/kit.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* An Effect-native redirect. Yield via {@link SvelteKit.redirect}; translated
|
|
6
|
+
* at the request boundary by {@link SvelteKit.translate}.
|
|
7
|
+
*/
|
|
8
|
+
declare class KitRedirect extends Error {
|
|
9
|
+
readonly status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308;
|
|
10
|
+
readonly location: string | URL;
|
|
11
|
+
readonly _tag: "KitRedirect";
|
|
12
|
+
constructor(status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string | URL);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* An Effect-native HTTP error. Yield via {@link SvelteKit.error}; translated
|
|
16
|
+
* at the request boundary by {@link SvelteKit.translate}.
|
|
17
|
+
*/
|
|
18
|
+
declare class KitError extends Error {
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly body?: string | Record<string, unknown> | undefined;
|
|
21
|
+
readonly _tag: "KitError";
|
|
22
|
+
constructor(status: number, body?: string | Record<string, unknown> | undefined);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An Effect-native form-action failure. Yield via {@link SvelteKit.fail};
|
|
26
|
+
* translated at the request boundary by {@link SvelteKit.translate}.
|
|
27
|
+
*/
|
|
28
|
+
declare class KitActionFailure<T extends Record<string, unknown> = Record<string, unknown>> extends Error {
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly data: T;
|
|
31
|
+
readonly _tag: "KitActionFailure";
|
|
32
|
+
constructor(status: number, data: T);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A single domain-error case for {@link SvelteKit.makeTranslator}.
|
|
36
|
+
*
|
|
37
|
+
* Receives the squashed error value. Throw (e.g. `error()` / `redirect()`) to
|
|
38
|
+
* handle the case; return normally to pass through to the next case.
|
|
39
|
+
*/
|
|
40
|
+
type KitTranslatorCase = (error: unknown) => undefined | void;
|
|
41
|
+
/**
|
|
42
|
+
* A boundary translator: turn a failed `Cause` into thrown SvelteKit control
|
|
43
|
+
* flow or an `ActionFailure` return value.
|
|
44
|
+
*
|
|
45
|
+
* The Kit base translator ({@link SvelteKit.translate}) handles
|
|
46
|
+
* {@link KitRedirect}, {@link KitError}, and {@link KitActionFailure}. Build a
|
|
47
|
+
* custom translator via {@link SvelteKit.makeTranslator}.
|
|
48
|
+
*/
|
|
49
|
+
type KitErrorTranslator = (cause: Cause.Cause<unknown>) => never;
|
|
50
|
+
/**
|
|
51
|
+
* Effect-native SvelteKit control-flow helpers and the default boundary
|
|
52
|
+
* translator.
|
|
53
|
+
*
|
|
54
|
+
* **Error types**: {@link KitRedirect}, {@link KitError}, {@link KitActionFailure}
|
|
55
|
+
* are tagged Effect failures — use them with `Effect.catchTag`, `Match.tag`,
|
|
56
|
+
* and `instanceof` checks.
|
|
57
|
+
*
|
|
58
|
+
* **Default translator**: `SvelteKit.translate` is wired in as the default
|
|
59
|
+
* `translateError` for every server wrapper (`serverLoad`, `action`,
|
|
60
|
+
* `makeRuntimeAdapter`, `RemoteFunction.withRuntime`, etc.).
|
|
61
|
+
*/
|
|
62
|
+
declare const SvelteKit: {
|
|
63
|
+
/**
|
|
64
|
+
* Fail with a redirect. Equivalent to SvelteKit's `redirect()` but as an
|
|
65
|
+
* Effect failure instead of a thrown exception.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* yield* SvelteKit.redirect(303, "/login");
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
readonly redirect: (status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308, location: string | URL) => Effect.Effect<never, KitRedirect>;
|
|
73
|
+
/**
|
|
74
|
+
* Fail with an HTTP error. Equivalent to SvelteKit's `error()` but as an
|
|
75
|
+
* Effect failure instead of a thrown exception.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* yield* SvelteKit.error(404, "Not found");
|
|
80
|
+
* yield* SvelteKit.error(403, { message: "Forbidden" });
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
readonly error: (status: number, body?: string | Record<string, unknown>) => Effect.Effect<never, KitError>;
|
|
84
|
+
/**
|
|
85
|
+
* Fail with a form-action failure. Equivalent to SvelteKit's `fail()` but as
|
|
86
|
+
* an Effect failure instead of a return value.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* yield* SvelteKit.fail(422, { email: "already taken" });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
readonly fail: <T extends Record<string, unknown>>(status: number, data: T) => Effect.Effect<never, KitActionFailure<T>>;
|
|
94
|
+
/**
|
|
95
|
+
* The turnkey boundary translator. Converts {@link KitRedirect} →
|
|
96
|
+
* `redirect()`, {@link KitError} → `error()`, {@link KitActionFailure} →
|
|
97
|
+
* `fail()`, and anything else → `EffectCauseError` (500).
|
|
98
|
+
*
|
|
99
|
+
* This is the default `translateError` for every server wrapper. To extend it
|
|
100
|
+
* with your own domain errors, use {@link SvelteKit.makeTranslator}.
|
|
101
|
+
*/
|
|
102
|
+
readonly translate: KitErrorTranslator;
|
|
103
|
+
/**
|
|
104
|
+
* Build a custom boundary translator by prepending domain-error cases before
|
|
105
|
+
* the Kit base cases.
|
|
106
|
+
*
|
|
107
|
+
* Each case function receives the squashed error value. Throw (e.g. call
|
|
108
|
+
* `error()` / `redirect()` from `@sveltejs/kit`) to handle the case; return
|
|
109
|
+
* normally to pass through to the next case or the Kit base translator.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* import { error } from "@sveltejs/kit";
|
|
114
|
+
*
|
|
115
|
+
* const translateError = SvelteKit.makeTranslator([
|
|
116
|
+
* (e) => { if (e instanceof NotFound) error(404, "Not found"); },
|
|
117
|
+
* (e) => { if (e instanceof Forbidden) error(403, "Forbidden"); },
|
|
118
|
+
* ]);
|
|
119
|
+
*
|
|
120
|
+
* const { serverLoad } = makeRuntimeAdapter(
|
|
121
|
+
* RequestRuntime.fromLayer(AppLayer),
|
|
122
|
+
* { translateError },
|
|
123
|
+
* );
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
readonly makeTranslator: (extraCases: ReadonlyArray<KitTranslatorCase>) => KitErrorTranslator;
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
export { KitActionFailure, KitError, KitErrorTranslator, KitRedirect, KitTranslatorCase, SvelteKit };
|
|
130
|
+
//# sourceMappingURL=kit.d.ts.map
|
package/dist/kit.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { causeToThrowable } from "./internal/error.js";
|
|
2
|
+
import { Cause, Effect } from "effect";
|
|
3
|
+
import { error, fail, redirect } from "@sveltejs/kit";
|
|
4
|
+
//#region src/kit.ts
|
|
5
|
+
/**
|
|
6
|
+
* Effect-native SvelteKit control-flow errors and a turnkey boundary translator.
|
|
7
|
+
*
|
|
8
|
+
* Every server consumer previously needed to hand-roll their own `translateError`
|
|
9
|
+
* that mapped domain errors → `redirect()` / `error()` / `fail()`. This module
|
|
10
|
+
* ships the catalogue those consumers all reinvented:
|
|
11
|
+
*
|
|
12
|
+
* - {@link KitRedirect}, {@link KitError}, {@link KitActionFailure} — tagged
|
|
13
|
+
* Effect failure types for each SvelteKit control-flow call.
|
|
14
|
+
* - {@link SvelteKit.redirect}, {@link SvelteKit.error}, {@link SvelteKit.fail}
|
|
15
|
+
* — helpers that `yield*` a typed failure instead of throwing.
|
|
16
|
+
* - {@link SvelteKit.translate} — the base `ErrorTranslator` that converts those
|
|
17
|
+
* failures to the appropriate SvelteKit call at the request boundary.
|
|
18
|
+
* - {@link SvelteKit.makeTranslator} — compose `translate` with your own
|
|
19
|
+
* domain-error cases (consumer cases run first).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { SvelteKit, makeRuntimeAdapter, RequestRuntime } from "@thomasfosterau/effect-sveltekit/server";
|
|
24
|
+
* import { Effect } from "effect";
|
|
25
|
+
*
|
|
26
|
+
* // Yield kit errors instead of throwing:
|
|
27
|
+
* const load = serverLoad(() =>
|
|
28
|
+
* Effect.gen(function* () {
|
|
29
|
+
* const user = yield* getUser;
|
|
30
|
+
* if (!user) yield* SvelteKit.redirect(303, "/login");
|
|
31
|
+
* if (!user.active) yield* SvelteKit.error(403, "Account disabled");
|
|
32
|
+
* return { user };
|
|
33
|
+
* })
|
|
34
|
+
* );
|
|
35
|
+
*
|
|
36
|
+
* // Compose with your own domain errors:
|
|
37
|
+
* import { error } from "@sveltejs/kit";
|
|
38
|
+
* const { serverLoad } = makeRuntimeAdapter(
|
|
39
|
+
* RequestRuntime.fromLayer(AppLayer),
|
|
40
|
+
* {
|
|
41
|
+
* translateError: SvelteKit.makeTranslator([
|
|
42
|
+
* (e) => { if (e instanceof NotFound) error(404, "Not found"); },
|
|
43
|
+
* ]),
|
|
44
|
+
* },
|
|
45
|
+
* );
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* An Effect-native redirect. Yield via {@link SvelteKit.redirect}; translated
|
|
50
|
+
* at the request boundary by {@link SvelteKit.translate}.
|
|
51
|
+
*/
|
|
52
|
+
var KitRedirect = class extends Error {
|
|
53
|
+
status;
|
|
54
|
+
location;
|
|
55
|
+
_tag = "KitRedirect";
|
|
56
|
+
constructor(status, location) {
|
|
57
|
+
super(`Redirect to ${String(location)}`);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.location = location;
|
|
60
|
+
this.name = "KitRedirect";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* An Effect-native HTTP error. Yield via {@link SvelteKit.error}; translated
|
|
65
|
+
* at the request boundary by {@link SvelteKit.translate}.
|
|
66
|
+
*/
|
|
67
|
+
var KitError = class extends Error {
|
|
68
|
+
status;
|
|
69
|
+
body;
|
|
70
|
+
_tag = "KitError";
|
|
71
|
+
constructor(status, body) {
|
|
72
|
+
super(`HTTP ${status}`);
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.body = body;
|
|
75
|
+
this.name = "KitError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* An Effect-native form-action failure. Yield via {@link SvelteKit.fail};
|
|
80
|
+
* translated at the request boundary by {@link SvelteKit.translate}.
|
|
81
|
+
*/
|
|
82
|
+
var KitActionFailure = class extends Error {
|
|
83
|
+
status;
|
|
84
|
+
data;
|
|
85
|
+
_tag = "KitActionFailure";
|
|
86
|
+
constructor(status, data) {
|
|
87
|
+
super(`Action failure ${status}`);
|
|
88
|
+
this.status = status;
|
|
89
|
+
this.data = data;
|
|
90
|
+
this.name = "KitActionFailure";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const baseTranslate = (cause) => {
|
|
94
|
+
const squashed = Cause.squash(cause);
|
|
95
|
+
if (squashed instanceof KitRedirect) redirect(squashed.status, squashed.location);
|
|
96
|
+
if (squashed instanceof KitError) error(squashed.status, squashed.body);
|
|
97
|
+
if (squashed instanceof KitActionFailure) return fail(squashed.status, squashed.data);
|
|
98
|
+
throw causeToThrowable(cause);
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Effect-native SvelteKit control-flow helpers and the default boundary
|
|
102
|
+
* translator.
|
|
103
|
+
*
|
|
104
|
+
* **Error types**: {@link KitRedirect}, {@link KitError}, {@link KitActionFailure}
|
|
105
|
+
* are tagged Effect failures — use them with `Effect.catchTag`, `Match.tag`,
|
|
106
|
+
* and `instanceof` checks.
|
|
107
|
+
*
|
|
108
|
+
* **Default translator**: `SvelteKit.translate` is wired in as the default
|
|
109
|
+
* `translateError` for every server wrapper (`serverLoad`, `action`,
|
|
110
|
+
* `makeRuntimeAdapter`, `RemoteFunction.withRuntime`, etc.).
|
|
111
|
+
*/
|
|
112
|
+
const SvelteKit = {
|
|
113
|
+
/**
|
|
114
|
+
* Fail with a redirect. Equivalent to SvelteKit's `redirect()` but as an
|
|
115
|
+
* Effect failure instead of a thrown exception.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* yield* SvelteKit.redirect(303, "/login");
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
redirect: (status, location) => Effect.fail(new KitRedirect(status, location)),
|
|
123
|
+
/**
|
|
124
|
+
* Fail with an HTTP error. Equivalent to SvelteKit's `error()` but as an
|
|
125
|
+
* Effect failure instead of a thrown exception.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* yield* SvelteKit.error(404, "Not found");
|
|
130
|
+
* yield* SvelteKit.error(403, { message: "Forbidden" });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
error: (status, body) => Effect.fail(new KitError(status, body)),
|
|
134
|
+
/**
|
|
135
|
+
* Fail with a form-action failure. Equivalent to SvelteKit's `fail()` but as
|
|
136
|
+
* an Effect failure instead of a return value.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* yield* SvelteKit.fail(422, { email: "already taken" });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
fail: (status, data) => Effect.fail(new KitActionFailure(status, data)),
|
|
144
|
+
/**
|
|
145
|
+
* The turnkey boundary translator. Converts {@link KitRedirect} →
|
|
146
|
+
* `redirect()`, {@link KitError} → `error()`, {@link KitActionFailure} →
|
|
147
|
+
* `fail()`, and anything else → `EffectCauseError` (500).
|
|
148
|
+
*
|
|
149
|
+
* This is the default `translateError` for every server wrapper. To extend it
|
|
150
|
+
* with your own domain errors, use {@link SvelteKit.makeTranslator}.
|
|
151
|
+
*/
|
|
152
|
+
translate: baseTranslate,
|
|
153
|
+
/**
|
|
154
|
+
* Build a custom boundary translator by prepending domain-error cases before
|
|
155
|
+
* the Kit base cases.
|
|
156
|
+
*
|
|
157
|
+
* Each case function receives the squashed error value. Throw (e.g. call
|
|
158
|
+
* `error()` / `redirect()` from `@sveltejs/kit`) to handle the case; return
|
|
159
|
+
* normally to pass through to the next case or the Kit base translator.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* import { error } from "@sveltejs/kit";
|
|
164
|
+
*
|
|
165
|
+
* const translateError = SvelteKit.makeTranslator([
|
|
166
|
+
* (e) => { if (e instanceof NotFound) error(404, "Not found"); },
|
|
167
|
+
* (e) => { if (e instanceof Forbidden) error(403, "Forbidden"); },
|
|
168
|
+
* ]);
|
|
169
|
+
*
|
|
170
|
+
* const { serverLoad } = makeRuntimeAdapter(
|
|
171
|
+
* RequestRuntime.fromLayer(AppLayer),
|
|
172
|
+
* { translateError },
|
|
173
|
+
* );
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
makeTranslator: (extraCases) => (cause) => {
|
|
177
|
+
const squashed = Cause.squash(cause);
|
|
178
|
+
for (const matcher of extraCases) matcher(squashed);
|
|
179
|
+
return baseTranslate(cause);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
//#endregion
|
|
183
|
+
export { KitActionFailure, KitError, KitRedirect, SvelteKit };
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=kit.js.map
|
package/dist/kit.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kit.js","names":["kitFail"],"sources":["../src/kit.ts"],"sourcesContent":["/**\n * Effect-native SvelteKit control-flow errors and a turnkey boundary translator.\n *\n * Every server consumer previously needed to hand-roll their own `translateError`\n * that mapped domain errors → `redirect()` / `error()` / `fail()`. This module\n * ships the catalogue those consumers all reinvented:\n *\n * - {@link KitRedirect}, {@link KitError}, {@link KitActionFailure} — tagged\n * Effect failure types for each SvelteKit control-flow call.\n * - {@link SvelteKit.redirect}, {@link SvelteKit.error}, {@link SvelteKit.fail}\n * — helpers that `yield*` a typed failure instead of throwing.\n * - {@link SvelteKit.translate} — the base `ErrorTranslator` that converts those\n * failures to the appropriate SvelteKit call at the request boundary.\n * - {@link SvelteKit.makeTranslator} — compose `translate` with your own\n * domain-error cases (consumer cases run first).\n *\n * @example\n * ```ts\n * import { SvelteKit, makeRuntimeAdapter, RequestRuntime } from \"@thomasfosterau/effect-sveltekit/server\";\n * import { Effect } from \"effect\";\n *\n * // Yield kit errors instead of throwing:\n * const load = serverLoad(() =>\n * Effect.gen(function* () {\n * const user = yield* getUser;\n * if (!user) yield* SvelteKit.redirect(303, \"/login\");\n * if (!user.active) yield* SvelteKit.error(403, \"Account disabled\");\n * return { user };\n * })\n * );\n *\n * // Compose with your own domain errors:\n * import { error } from \"@sveltejs/kit\";\n * const { serverLoad } = makeRuntimeAdapter(\n * RequestRuntime.fromLayer(AppLayer),\n * {\n * translateError: SvelteKit.makeTranslator([\n * (e) => { if (e instanceof NotFound) error(404, \"Not found\"); },\n * ]),\n * },\n * );\n * ```\n */\n\nimport { error as kitError, fail as kitFail, redirect as kitRedirect } from \"@sveltejs/kit\";\nimport { Cause, Effect } from \"effect\";\nimport { causeToThrowable } from \"./internal/error.js\";\n\n// ─── Tagged error classes ────────────────────────────────────────────────────\n\n/**\n * An Effect-native redirect. Yield via {@link SvelteKit.redirect}; translated\n * at the request boundary by {@link SvelteKit.translate}.\n */\nexport class KitRedirect extends Error {\n readonly _tag = \"KitRedirect\" as const;\n\n constructor(\n readonly status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308,\n readonly location: string | URL,\n ) {\n super(`Redirect to ${String(location)}`);\n this.name = \"KitRedirect\";\n }\n}\n\n/**\n * An Effect-native HTTP error. Yield via {@link SvelteKit.error}; translated\n * at the request boundary by {@link SvelteKit.translate}.\n */\nexport class KitError extends Error {\n readonly _tag = \"KitError\" as const;\n\n constructor(\n readonly status: number,\n readonly body?: string | Record<string, unknown>,\n ) {\n super(`HTTP ${status}`);\n this.name = \"KitError\";\n }\n}\n\n/**\n * An Effect-native form-action failure. Yield via {@link SvelteKit.fail};\n * translated at the request boundary by {@link SvelteKit.translate}.\n */\nexport class KitActionFailure<\n T extends Record<string, unknown> = Record<string, unknown>,\n> extends Error {\n readonly _tag = \"KitActionFailure\" as const;\n\n constructor(\n readonly status: number,\n readonly data: T,\n ) {\n super(`Action failure ${status}`);\n this.name = \"KitActionFailure\";\n }\n}\n\n// ─── Translator types ────────────────────────────────────────────────────────\n\n/**\n * A single domain-error case for {@link SvelteKit.makeTranslator}.\n *\n * Receives the squashed error value. Throw (e.g. `error()` / `redirect()`) to\n * handle the case; return normally to pass through to the next case.\n */\nexport type KitTranslatorCase = (error: unknown) => undefined | void;\n\n/**\n * A boundary translator: turn a failed `Cause` into thrown SvelteKit control\n * flow or an `ActionFailure` return value.\n *\n * The Kit base translator ({@link SvelteKit.translate}) handles\n * {@link KitRedirect}, {@link KitError}, and {@link KitActionFailure}. Build a\n * custom translator via {@link SvelteKit.makeTranslator}.\n */\nexport type KitErrorTranslator = (cause: Cause.Cause<unknown>) => never;\n\n// ─── Base translator ─────────────────────────────────────────────────────────\n\nconst baseTranslate: KitErrorTranslator = (cause) => {\n const squashed = Cause.squash(cause);\n\n if (squashed instanceof KitRedirect) {\n // redirect() is typed as () => never — throws internally.\n kitRedirect(squashed.status, squashed.location as string);\n }\n\n if (squashed instanceof KitError) {\n // error() is typed as () => never — throws internally.\n kitError(squashed.status as Parameters<typeof kitError>[0], squashed.body as any);\n }\n\n if (squashed instanceof KitActionFailure) {\n // fail() returns an ActionFailure (doesn't throw). SvelteKit detects it as\n // the action's return value. Typed as never to satisfy ErrorTranslator while\n // allowing makeRuntimeAdapter's `return translateError(cause)` to propagate\n // the ActionFailure to SvelteKit correctly at runtime.\n return kitFail(squashed.status, squashed.data) as never;\n }\n\n throw causeToThrowable(cause);\n};\n\n// ─── Public namespace ─────────────────────────────────────────────────────────\n\n/**\n * Effect-native SvelteKit control-flow helpers and the default boundary\n * translator.\n *\n * **Error types**: {@link KitRedirect}, {@link KitError}, {@link KitActionFailure}\n * are tagged Effect failures — use them with `Effect.catchTag`, `Match.tag`,\n * and `instanceof` checks.\n *\n * **Default translator**: `SvelteKit.translate` is wired in as the default\n * `translateError` for every server wrapper (`serverLoad`, `action`,\n * `makeRuntimeAdapter`, `RemoteFunction.withRuntime`, etc.).\n */\nexport const SvelteKit = {\n /**\n * Fail with a redirect. Equivalent to SvelteKit's `redirect()` but as an\n * Effect failure instead of a thrown exception.\n *\n * @example\n * ```ts\n * yield* SvelteKit.redirect(303, \"/login\");\n * ```\n */\n redirect: (\n status: 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308,\n location: string | URL,\n ): Effect.Effect<never, KitRedirect> => Effect.fail(new KitRedirect(status, location)),\n\n /**\n * Fail with an HTTP error. Equivalent to SvelteKit's `error()` but as an\n * Effect failure instead of a thrown exception.\n *\n * @example\n * ```ts\n * yield* SvelteKit.error(404, \"Not found\");\n * yield* SvelteKit.error(403, { message: \"Forbidden\" });\n * ```\n */\n error: (\n status: number,\n body?: string | Record<string, unknown>,\n ): Effect.Effect<never, KitError> => Effect.fail(new KitError(status, body)),\n\n /**\n * Fail with a form-action failure. Equivalent to SvelteKit's `fail()` but as\n * an Effect failure instead of a return value.\n *\n * @example\n * ```ts\n * yield* SvelteKit.fail(422, { email: \"already taken\" });\n * ```\n */\n fail: <T extends Record<string, unknown>>(\n status: number,\n data: T,\n ): Effect.Effect<never, KitActionFailure<T>> => Effect.fail(new KitActionFailure(status, data)),\n\n /**\n * The turnkey boundary translator. Converts {@link KitRedirect} →\n * `redirect()`, {@link KitError} → `error()`, {@link KitActionFailure} →\n * `fail()`, and anything else → `EffectCauseError` (500).\n *\n * This is the default `translateError` for every server wrapper. To extend it\n * with your own domain errors, use {@link SvelteKit.makeTranslator}.\n */\n translate: baseTranslate,\n\n /**\n * Build a custom boundary translator by prepending domain-error cases before\n * the Kit base cases.\n *\n * Each case function receives the squashed error value. Throw (e.g. call\n * `error()` / `redirect()` from `@sveltejs/kit`) to handle the case; return\n * normally to pass through to the next case or the Kit base translator.\n *\n * @example\n * ```ts\n * import { error } from \"@sveltejs/kit\";\n *\n * const translateError = SvelteKit.makeTranslator([\n * (e) => { if (e instanceof NotFound) error(404, \"Not found\"); },\n * (e) => { if (e instanceof Forbidden) error(403, \"Forbidden\"); },\n * ]);\n *\n * const { serverLoad } = makeRuntimeAdapter(\n * RequestRuntime.fromLayer(AppLayer),\n * { translateError },\n * );\n * ```\n */\n makeTranslator:\n (extraCases: ReadonlyArray<KitTranslatorCase>): KitErrorTranslator =>\n (cause) => {\n const squashed = Cause.squash(cause);\n for (const matcher of extraCases) {\n matcher(squashed);\n }\n return baseTranslate(cause);\n },\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,IAAa,cAAb,cAAiC,MAAM;CAI1B;CACA;CAJX,OAAgB;CAEhB,YACE,QACA,UACA;EACA,MAAM,eAAe,OAAO,QAAQ,GAAG;EAH9B,KAAA,SAAA;EACA,KAAA,WAAA;EAGT,KAAK,OAAO;CACd;AACF;;;;;AAMA,IAAa,WAAb,cAA8B,MAAM;CAIvB;CACA;CAJX,OAAgB;CAEhB,YACE,QACA,MACA;EACA,MAAM,QAAQ,QAAQ;EAHb,KAAA,SAAA;EACA,KAAA,OAAA;EAGT,KAAK,OAAO;CACd;AACF;;;;;AAMA,IAAa,mBAAb,cAEU,MAAM;CAIH;CACA;CAJX,OAAgB;CAEhB,YACE,QACA,MACA;EACA,MAAM,kBAAkB,QAAQ;EAHvB,KAAA,SAAA;EACA,KAAA,OAAA;EAGT,KAAK,OAAO;CACd;AACF;AAwBA,MAAM,iBAAqC,UAAU;CACnD,MAAM,WAAW,MAAM,OAAO,KAAK;CAEnC,IAAI,oBAAoB,aAEtB,SAAY,SAAS,QAAQ,SAAS,QAAkB;CAG1D,IAAI,oBAAoB,UAEtB,MAAS,SAAS,QAA0C,SAAS,IAAW;CAGlF,IAAI,oBAAoB,kBAKtB,OAAOA,KAAQ,SAAS,QAAQ,SAAS,IAAI;CAG/C,MAAM,iBAAiB,KAAK;AAC9B;;;;;;;;;;;;;AAgBA,MAAa,YAAY;;;;;;;;;;CAUvB,WACE,QACA,aACsC,OAAO,KAAK,IAAI,YAAY,QAAQ,QAAQ,CAAC;;;;;;;;;;;CAYrF,QACE,QACA,SACmC,OAAO,KAAK,IAAI,SAAS,QAAQ,IAAI,CAAC;;;;;;;;;;CAW3E,OACE,QACA,SAC8C,OAAO,KAAK,IAAI,iBAAiB,QAAQ,IAAI,CAAC;;;;;;;;;CAU9F,WAAW;;;;;;;;;;;;;;;;;;;;;;;;CAyBX,iBACG,gBACA,UAAU;EACT,MAAM,WAAW,MAAM,OAAO,KAAK;EACnC,KAAK,MAAM,WAAW,YACpB,QAAQ,QAAQ;EAElB,OAAO,cAAc,KAAK;CAC5B;AACJ"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Cause, Stream } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/realtime.d.ts
|
|
4
|
+
declare namespace realtime_d_exports {
|
|
5
|
+
export { EventSourceConstructor, EventSourceError, FromEventSourceOptions, FromWebSocketOptions, SseMessage, WebSocketConstructor, WebSocketError, fromEventSource, fromWebSocket };
|
|
6
|
+
}
|
|
7
|
+
/** A decoded Server-Sent Event. */
|
|
8
|
+
interface SseMessage {
|
|
9
|
+
/** The event name (`"message"` for unnamed events). */
|
|
10
|
+
readonly event: string;
|
|
11
|
+
/** The event payload (the joined `data:` lines). */
|
|
12
|
+
readonly data: string;
|
|
13
|
+
/** The last event id seen, or `""` if none. */
|
|
14
|
+
readonly lastEventId: string;
|
|
15
|
+
}
|
|
16
|
+
declare const EventSourceError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => Cause.YieldableError & {
|
|
17
|
+
readonly _tag: "EventSourceError";
|
|
18
|
+
} & Readonly<A>;
|
|
19
|
+
/** Raised when an `EventSource` connection fails fatally (its `readyState` is `CLOSED`). */
|
|
20
|
+
declare class EventSourceError extends EventSourceError_base<{
|
|
21
|
+
readonly url: string;
|
|
22
|
+
}> {}
|
|
23
|
+
/** The subset of the `EventSource` constructor this module relies on. */
|
|
24
|
+
type EventSourceConstructor = new (url: string, eventSourceInitDict?: {
|
|
25
|
+
withCredentials?: boolean;
|
|
26
|
+
}) => EventSource;
|
|
27
|
+
interface FromEventSourceOptions {
|
|
28
|
+
/** Send credentials (cookies) with the request. Defaults to `false`. */
|
|
29
|
+
readonly withCredentials?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Named events to listen for in addition to the default unnamed `message`
|
|
32
|
+
* event. An `Endpoint.fromStream({ event: "tick" })` server needs
|
|
33
|
+
* `events: ["tick"]` here.
|
|
34
|
+
*/
|
|
35
|
+
readonly events?: ReadonlyArray<string>;
|
|
36
|
+
/** Buffer size for the backing queue. Defaults to 16. */
|
|
37
|
+
readonly bufferSize?: number;
|
|
38
|
+
/** Backpressure strategy when the buffer is full. Defaults to `"sliding"`. */
|
|
39
|
+
readonly strategy?: "sliding" | "dropping" | "suspend";
|
|
40
|
+
/** Override the `EventSource` constructor (Node polyfill, tests). */
|
|
41
|
+
readonly implementation?: EventSourceConstructor;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Consume a Server-Sent-Events endpoint as a `Stream` of {@link SseMessage}.
|
|
45
|
+
*
|
|
46
|
+
* The browser's `EventSource` reconnects automatically after transient drops,
|
|
47
|
+
* so the stream stays live across reconnects; it fails with
|
|
48
|
+
* {@link EventSourceError} only when the connection closes fatally.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { Realtime } from "@thomasfosterau/effect-sveltekit/client";
|
|
53
|
+
* import { Effect, Stream } from "effect";
|
|
54
|
+
*
|
|
55
|
+
* const program = Realtime.fromEventSource("/events", { events: ["tick"] }).pipe(
|
|
56
|
+
* Stream.runForEach((msg) => Effect.log(`${msg.event}: ${msg.data}`)),
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
declare const fromEventSource: (url: string, options?: FromEventSourceOptions) => Stream.Stream<SseMessage, EventSourceError>;
|
|
61
|
+
declare const WebSocketError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => Cause.YieldableError & {
|
|
62
|
+
readonly _tag: "WebSocketError";
|
|
63
|
+
} & Readonly<A>;
|
|
64
|
+
/** Raised when a `WebSocket` errors or closes uncleanly. */
|
|
65
|
+
declare class WebSocketError extends WebSocketError_base<{
|
|
66
|
+
readonly url: string;
|
|
67
|
+
readonly code?: number;
|
|
68
|
+
readonly reason?: string;
|
|
69
|
+
}> {}
|
|
70
|
+
/** The subset of the `WebSocket` constructor this module relies on. */
|
|
71
|
+
type WebSocketConstructor = new (url: string, protocols?: string | ReadonlyArray<string>) => WebSocket;
|
|
72
|
+
interface FromWebSocketOptions {
|
|
73
|
+
/** Sub-protocols passed to the `WebSocket` constructor. */
|
|
74
|
+
readonly protocols?: string | ReadonlyArray<string>;
|
|
75
|
+
/** `arraybuffer` or `blob` for binary frames. Defaults to the platform default. */
|
|
76
|
+
readonly binaryType?: BinaryType;
|
|
77
|
+
/** Buffer size for the backing queue. Defaults to 16. */
|
|
78
|
+
readonly bufferSize?: number;
|
|
79
|
+
/** Backpressure strategy when the buffer is full. Defaults to `"suspend"`. */
|
|
80
|
+
readonly strategy?: "sliding" | "dropping" | "suspend";
|
|
81
|
+
/** Override the `WebSocket` constructor (Node polyfill, tests). */
|
|
82
|
+
readonly implementation?: WebSocketConstructor;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Consume a `WebSocket` as a `Stream` of its message payloads
|
|
86
|
+
* (`string | ArrayBuffer | Blob`, depending on `binaryType`).
|
|
87
|
+
*
|
|
88
|
+
* The stream ends when the socket closes cleanly and fails with
|
|
89
|
+
* {@link WebSocketError} on an error or unclean close. The socket is closed when
|
|
90
|
+
* the stream's scope closes.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { Realtime } from "@thomasfosterau/effect-sveltekit/client";
|
|
95
|
+
* import { Effect, Stream } from "effect";
|
|
96
|
+
*
|
|
97
|
+
* const program = Realtime.fromWebSocket("wss://example.com/ws").pipe(
|
|
98
|
+
* Stream.runForEach((data) => Effect.log(String(data))),
|
|
99
|
+
* );
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
declare const fromWebSocket: (url: string, options?: FromWebSocketOptions) => Stream.Stream<string | ArrayBuffer | Blob, WebSocketError>;
|
|
103
|
+
//#endregion
|
|
104
|
+
export { realtime_d_exports };
|
|
105
|
+
//# sourceMappingURL=realtime.d.ts.map
|
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { Cause, Data, Effect, Queue, Stream } from "effect";
|
|
3
|
+
//#region src/realtime.ts
|
|
4
|
+
/**
|
|
5
|
+
* Client-side realtime bridges: consume a Server-Sent-Events endpoint or a
|
|
6
|
+
* WebSocket as an Effect `Stream`. The counterpart to `Endpoint.fromStream`
|
|
7
|
+
* (server) on the `@thomasfosterau/effect-sveltekit/client` `Realtime` namespace.
|
|
8
|
+
*
|
|
9
|
+
* These use the browser globals `EventSource` / `WebSocket`; both can be
|
|
10
|
+
* overridden (e.g. a Node polyfill, or a fake in tests) via the `implementation`
|
|
11
|
+
* option. The source is opened when the stream is run and closed when its scope
|
|
12
|
+
* closes, so a `Stream.take` / component teardown tears down the connection.
|
|
13
|
+
*/
|
|
14
|
+
var realtime_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
EventSourceError: () => EventSourceError,
|
|
16
|
+
WebSocketError: () => WebSocketError,
|
|
17
|
+
fromEventSource: () => fromEventSource,
|
|
18
|
+
fromWebSocket: () => fromWebSocket
|
|
19
|
+
});
|
|
20
|
+
/** Raised when an `EventSource` connection fails fatally (its `readyState` is `CLOSED`). */
|
|
21
|
+
var EventSourceError = class extends Data.TaggedError("EventSourceError") {};
|
|
22
|
+
/**
|
|
23
|
+
* Consume a Server-Sent-Events endpoint as a `Stream` of {@link SseMessage}.
|
|
24
|
+
*
|
|
25
|
+
* The browser's `EventSource` reconnects automatically after transient drops,
|
|
26
|
+
* so the stream stays live across reconnects; it fails with
|
|
27
|
+
* {@link EventSourceError} only when the connection closes fatally.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { Realtime } from "@thomasfosterau/effect-sveltekit/client";
|
|
32
|
+
* import { Effect, Stream } from "effect";
|
|
33
|
+
*
|
|
34
|
+
* const program = Realtime.fromEventSource("/events", { events: ["tick"] }).pipe(
|
|
35
|
+
* Stream.runForEach((msg) => Effect.log(`${msg.event}: ${msg.data}`)),
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
const fromEventSource = (url, options) => {
|
|
40
|
+
const Impl = options?.implementation ?? globalThis.EventSource;
|
|
41
|
+
return Stream.callback((queue) => Effect.sync(() => {
|
|
42
|
+
const source = options?.withCredentials === void 0 ? new Impl(url) : new Impl(url, { withCredentials: options.withCredentials });
|
|
43
|
+
const onMessage = (e) => {
|
|
44
|
+
Queue.offerUnsafe(queue, {
|
|
45
|
+
event: e.type,
|
|
46
|
+
data: e.data,
|
|
47
|
+
lastEventId: e.lastEventId
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
source.addEventListener("message", onMessage);
|
|
51
|
+
for (const name of options?.events ?? []) source.addEventListener(name, onMessage);
|
|
52
|
+
source.addEventListener("error", () => {
|
|
53
|
+
if (source.readyState === 2) Queue.failCauseUnsafe(queue, Cause.fail(new EventSourceError({ url })));
|
|
54
|
+
});
|
|
55
|
+
return Effect.sync(() => source.close());
|
|
56
|
+
}), {
|
|
57
|
+
bufferSize: options?.bufferSize ?? 16,
|
|
58
|
+
strategy: options?.strategy ?? "sliding"
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
/** Raised when a `WebSocket` errors or closes uncleanly. */
|
|
62
|
+
var WebSocketError = class extends Data.TaggedError("WebSocketError") {};
|
|
63
|
+
/**
|
|
64
|
+
* Consume a `WebSocket` as a `Stream` of its message payloads
|
|
65
|
+
* (`string | ArrayBuffer | Blob`, depending on `binaryType`).
|
|
66
|
+
*
|
|
67
|
+
* The stream ends when the socket closes cleanly and fails with
|
|
68
|
+
* {@link WebSocketError} on an error or unclean close. The socket is closed when
|
|
69
|
+
* the stream's scope closes.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { Realtime } from "@thomasfosterau/effect-sveltekit/client";
|
|
74
|
+
* import { Effect, Stream } from "effect";
|
|
75
|
+
*
|
|
76
|
+
* const program = Realtime.fromWebSocket("wss://example.com/ws").pipe(
|
|
77
|
+
* Stream.runForEach((data) => Effect.log(String(data))),
|
|
78
|
+
* );
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
const fromWebSocket = (url, options) => {
|
|
82
|
+
const Impl = options?.implementation ?? globalThis.WebSocket;
|
|
83
|
+
return Stream.callback((queue) => Effect.sync(() => {
|
|
84
|
+
const socket = new Impl(url, options?.protocols);
|
|
85
|
+
if (options?.binaryType !== void 0) socket.binaryType = options.binaryType;
|
|
86
|
+
socket.addEventListener("message", (e) => {
|
|
87
|
+
Queue.offerUnsafe(queue, e.data);
|
|
88
|
+
});
|
|
89
|
+
socket.addEventListener("error", () => {
|
|
90
|
+
Queue.failCauseUnsafe(queue, Cause.fail(new WebSocketError({ url })));
|
|
91
|
+
});
|
|
92
|
+
socket.addEventListener("close", (e) => {
|
|
93
|
+
if (e.wasClean) Queue.endUnsafe(queue);
|
|
94
|
+
else Queue.failCauseUnsafe(queue, Cause.fail(new WebSocketError({
|
|
95
|
+
url,
|
|
96
|
+
code: e.code,
|
|
97
|
+
reason: e.reason
|
|
98
|
+
})));
|
|
99
|
+
});
|
|
100
|
+
return Effect.sync(() => {
|
|
101
|
+
if (socket.readyState === 0 || socket.readyState === 1) socket.close();
|
|
102
|
+
});
|
|
103
|
+
}), {
|
|
104
|
+
bufferSize: options?.bufferSize ?? 16,
|
|
105
|
+
strategy: options?.strategy ?? "suspend"
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { realtime_exports };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=realtime.js.map
|