@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":"realtime.js","names":[],"sources":["../src/realtime.ts"],"sourcesContent":["/**\n * Client-side realtime bridges: consume a Server-Sent-Events endpoint or a\n * WebSocket as an Effect `Stream`. The counterpart to `Endpoint.fromStream`\n * (server) on the `@thomasfosterau/effect-sveltekit/client` `Realtime` namespace.\n *\n * These use the browser globals `EventSource` / `WebSocket`; both can be\n * overridden (e.g. a Node polyfill, or a fake in tests) via the `implementation`\n * option. The source is opened when the stream is run and closed when its scope\n * closes, so a `Stream.take` / component teardown tears down the connection.\n */\n\nimport { Cause, Data, Effect, Queue, Stream } from \"effect\";\n\n// ---------------------------------------------------------------------------\n// EventSource\n// ---------------------------------------------------------------------------\n\n/** A decoded Server-Sent Event. */\nexport interface SseMessage {\n /** The event name (`\"message\"` for unnamed events). */\n readonly event: string;\n /** The event payload (the joined `data:` lines). */\n readonly data: string;\n /** The last event id seen, or `\"\"` if none. */\n readonly lastEventId: string;\n}\n\n/** Raised when an `EventSource` connection fails fatally (its `readyState` is `CLOSED`). */\nexport class EventSourceError extends Data.TaggedError(\"EventSourceError\")<{\n readonly url: string;\n}> {}\n\n/** The subset of the `EventSource` constructor this module relies on. */\nexport type EventSourceConstructor = new (\n url: string,\n eventSourceInitDict?: { withCredentials?: boolean },\n) => EventSource;\n\nexport interface FromEventSourceOptions {\n /** Send credentials (cookies) with the request. Defaults to `false`. */\n readonly withCredentials?: boolean;\n /**\n * Named events to listen for in addition to the default unnamed `message`\n * event. An `Endpoint.fromStream({ event: \"tick\" })` server needs\n * `events: [\"tick\"]` here.\n */\n readonly events?: ReadonlyArray<string>;\n /** Buffer size for the backing queue. Defaults to 16. */\n readonly bufferSize?: number;\n /** Backpressure strategy when the buffer is full. Defaults to `\"sliding\"`. */\n readonly strategy?: \"sliding\" | \"dropping\" | \"suspend\";\n /** Override the `EventSource` constructor (Node polyfill, tests). */\n readonly implementation?: EventSourceConstructor;\n}\n\n/**\n * Consume a Server-Sent-Events endpoint as a `Stream` of {@link SseMessage}.\n *\n * The browser's `EventSource` reconnects automatically after transient drops,\n * so the stream stays live across reconnects; it fails with\n * {@link EventSourceError} only when the connection closes fatally.\n *\n * @example\n * ```ts\n * import { Realtime } from \"@thomasfosterau/effect-sveltekit/client\";\n * import { Effect, Stream } from \"effect\";\n *\n * const program = Realtime.fromEventSource(\"/events\", { events: [\"tick\"] }).pipe(\n * Stream.runForEach((msg) => Effect.log(`${msg.event}: ${msg.data}`)),\n * );\n * ```\n */\nexport const fromEventSource = (\n url: string,\n options?: FromEventSourceOptions,\n): Stream.Stream<SseMessage, EventSourceError> => {\n const Impl = options?.implementation ?? (globalThis.EventSource as EventSourceConstructor);\n return Stream.callback<SseMessage, EventSourceError>(\n (queue) =>\n Effect.sync(() => {\n const source =\n options?.withCredentials === undefined\n ? new Impl(url)\n : new Impl(url, { withCredentials: options.withCredentials });\n\n const onMessage = (e: MessageEvent): void => {\n Queue.offerUnsafe(queue, { event: e.type, data: e.data, lastEventId: e.lastEventId });\n };\n\n source.addEventListener(\"message\", onMessage);\n for (const name of options?.events ?? []) source.addEventListener(name, onMessage);\n\n source.addEventListener(\"error\", () => {\n // EventSource reconnects on its own unless it has closed for good.\n if (source.readyState === 2 /* CLOSED */) {\n Queue.failCauseUnsafe(queue, Cause.fail(new EventSourceError({ url })));\n }\n });\n\n return Effect.sync(() => source.close());\n }),\n { bufferSize: options?.bufferSize ?? 16, strategy: options?.strategy ?? \"sliding\" },\n );\n};\n\n// ---------------------------------------------------------------------------\n// WebSocket\n// ---------------------------------------------------------------------------\n\n/** Raised when a `WebSocket` errors or closes uncleanly. */\nexport class WebSocketError extends Data.TaggedError(\"WebSocketError\")<{\n readonly url: string;\n readonly code?: number;\n readonly reason?: string;\n}> {}\n\n/** The subset of the `WebSocket` constructor this module relies on. */\nexport type WebSocketConstructor = new (\n url: string,\n protocols?: string | ReadonlyArray<string>,\n) => WebSocket;\n\nexport interface FromWebSocketOptions {\n /** Sub-protocols passed to the `WebSocket` constructor. */\n readonly protocols?: string | ReadonlyArray<string>;\n /** `arraybuffer` or `blob` for binary frames. Defaults to the platform default. */\n readonly binaryType?: BinaryType;\n /** Buffer size for the backing queue. Defaults to 16. */\n readonly bufferSize?: number;\n /** Backpressure strategy when the buffer is full. Defaults to `\"suspend\"`. */\n readonly strategy?: \"sliding\" | \"dropping\" | \"suspend\";\n /** Override the `WebSocket` constructor (Node polyfill, tests). */\n readonly implementation?: WebSocketConstructor;\n}\n\n/**\n * Consume a `WebSocket` as a `Stream` of its message payloads\n * (`string | ArrayBuffer | Blob`, depending on `binaryType`).\n *\n * The stream ends when the socket closes cleanly and fails with\n * {@link WebSocketError} on an error or unclean close. The socket is closed when\n * the stream's scope closes.\n *\n * @example\n * ```ts\n * import { Realtime } from \"@thomasfosterau/effect-sveltekit/client\";\n * import { Effect, Stream } from \"effect\";\n *\n * const program = Realtime.fromWebSocket(\"wss://example.com/ws\").pipe(\n * Stream.runForEach((data) => Effect.log(String(data))),\n * );\n * ```\n */\nexport const fromWebSocket = (\n url: string,\n options?: FromWebSocketOptions,\n): Stream.Stream<string | ArrayBuffer | Blob, WebSocketError> => {\n const Impl = options?.implementation ?? (globalThis.WebSocket as WebSocketConstructor);\n return Stream.callback<string | ArrayBuffer | Blob, WebSocketError>(\n (queue) =>\n Effect.sync(() => {\n const socket = new Impl(url, options?.protocols);\n if (options?.binaryType !== undefined) socket.binaryType = options.binaryType;\n\n socket.addEventListener(\"message\", (e: MessageEvent) => {\n Queue.offerUnsafe(queue, e.data);\n });\n\n socket.addEventListener(\"error\", () => {\n Queue.failCauseUnsafe(queue, Cause.fail(new WebSocketError({ url })));\n });\n\n socket.addEventListener(\"close\", (e: CloseEvent) => {\n if (e.wasClean) {\n Queue.endUnsafe(queue);\n } else {\n Queue.failCauseUnsafe(\n queue,\n Cause.fail(new WebSocketError({ url, code: e.code, reason: e.reason })),\n );\n }\n });\n\n return Effect.sync(() => {\n // Only close a socket that is still connecting/open.\n if (socket.readyState === 0 || socket.readyState === 1) socket.close();\n });\n }),\n { bufferSize: options?.bufferSize ?? 16, strategy: options?.strategy ?? \"suspend\" },\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,mBAAb,cAAsC,KAAK,YAAY,kBAAkB,EAEtE,CAAC;;;;;;;;;;;;;;;;;;AA0CJ,MAAa,mBACX,KACA,YACgD;CAChD,MAAM,OAAO,SAAS,kBAAmB,WAAW;CACpD,OAAO,OAAO,UACX,UACC,OAAO,WAAW;EAChB,MAAM,SACJ,SAAS,oBAAoB,KAAA,IACzB,IAAI,KAAK,GAAG,IACZ,IAAI,KAAK,KAAK,EAAE,iBAAiB,QAAQ,gBAAgB,CAAC;EAEhE,MAAM,aAAa,MAA0B;GAC3C,MAAM,YAAY,OAAO;IAAE,OAAO,EAAE;IAAM,MAAM,EAAE;IAAM,aAAa,EAAE;GAAY,CAAC;EACtF;EAEA,OAAO,iBAAiB,WAAW,SAAS;EAC5C,KAAK,MAAM,QAAQ,SAAS,UAAU,CAAC,GAAG,OAAO,iBAAiB,MAAM,SAAS;EAEjF,OAAO,iBAAiB,eAAe;GAErC,IAAI,OAAO,eAAe,GACxB,MAAM,gBAAgB,OAAO,MAAM,KAAK,IAAI,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;EAE1E,CAAC;EAED,OAAO,OAAO,WAAW,OAAO,MAAM,CAAC;CACzC,CAAC,GACH;EAAE,YAAY,SAAS,cAAc;EAAI,UAAU,SAAS,YAAY;CAAU,CACpF;AACF;;AAOA,IAAa,iBAAb,cAAoC,KAAK,YAAY,gBAAgB,EAIlE,CAAC;;;;;;;;;;;;;;;;;;;AAuCJ,MAAa,iBACX,KACA,YAC+D;CAC/D,MAAM,OAAO,SAAS,kBAAmB,WAAW;CACpD,OAAO,OAAO,UACX,UACC,OAAO,WAAW;EAChB,MAAM,SAAS,IAAI,KAAK,KAAK,SAAS,SAAS;EAC/C,IAAI,SAAS,eAAe,KAAA,GAAW,OAAO,aAAa,QAAQ;EAEnE,OAAO,iBAAiB,YAAY,MAAoB;GACtD,MAAM,YAAY,OAAO,EAAE,IAAI;EACjC,CAAC;EAED,OAAO,iBAAiB,eAAe;GACrC,MAAM,gBAAgB,OAAO,MAAM,KAAK,IAAI,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC;EACtE,CAAC;EAED,OAAO,iBAAiB,UAAU,MAAkB;GAClD,IAAI,EAAE,UACJ,MAAM,UAAU,KAAK;QAErB,MAAM,gBACJ,OACA,MAAM,KAAK,IAAI,eAAe;IAAE;IAAK,MAAM,EAAE;IAAM,QAAQ,EAAE;GAAO,CAAC,CAAC,CACxE;EAEJ,CAAC;EAED,OAAO,OAAO,WAAW;GAEvB,IAAI,OAAO,eAAe,KAAK,OAAO,eAAe,GAAG,OAAO,MAAM;EACvE,CAAC;CACH,CAAC,GACH;EAAE,YAAY,SAAS,cAAc;EAAI,UAAU,SAAS,YAAY;CAAU,CACpF;AACF"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SvelteKitRequestEvent } from "./runtime.js";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
|
|
5
|
+
//#region src/request-parse.d.ts
|
|
6
|
+
/** A single issue reported by a Standard Schema validator. */
|
|
7
|
+
interface ParseIssue {
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly path?: ReadonlyArray<PropertyKey | {
|
|
10
|
+
readonly key: PropertyKey;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
declare const ParseError_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] }>) => import("effect/Cause").YieldableError & {
|
|
14
|
+
readonly _tag: "ParseError";
|
|
15
|
+
} & Readonly<A>;
|
|
16
|
+
/**
|
|
17
|
+
* Raised when a schema validator rejects the request data.
|
|
18
|
+
*
|
|
19
|
+
* Carry the raw `issues` array from the Standard Schema validator so callers
|
|
20
|
+
* can pattern-match on specific field failures.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { ParseError } from '@thomasfosterau/effect-sveltekit/server';
|
|
25
|
+
* import { Effect } from 'effect';
|
|
26
|
+
*
|
|
27
|
+
* const result = yield* decodeJson(MySchema).pipe(
|
|
28
|
+
* Effect.catchTag("ParseError", (e) => Effect.succeed({ fallback: true })),
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
declare class ParseError extends ParseError_base<{
|
|
33
|
+
readonly issues: ReadonlyArray<ParseIssue>;
|
|
34
|
+
}> {}
|
|
35
|
+
/**
|
|
36
|
+
* Decode the route `params` object against `schema`.
|
|
37
|
+
*
|
|
38
|
+
* `params` is a plain `Record<string, string>` from the matched URL segments;
|
|
39
|
+
* the schema is free to coerce values (e.g. `Schema.NumberFromString`).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const Params = Schema.Struct({ id: Schema.String });
|
|
44
|
+
* const { id } = yield* decodeParams(Params);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
declare const decodeParams: <O>(schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>) => Effect.Effect<O, ParseError, SvelteKitRequestEvent>;
|
|
48
|
+
/**
|
|
49
|
+
* Decode the URL search / query parameters against `schema`.
|
|
50
|
+
*
|
|
51
|
+
* The raw value passed to the validator is a plain object built from
|
|
52
|
+
* `event.url.searchParams` (multiple values for the same key become an array).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const Query = Schema.Struct({ page: Schema.NumberFromString });
|
|
57
|
+
* const { page } = yield* decodeSearchParams(Query);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
declare const decodeSearchParams: <O>(schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>) => Effect.Effect<O, ParseError, SvelteKitRequestEvent>;
|
|
61
|
+
/**
|
|
62
|
+
* Parse the request body as JSON and decode it against `schema`.
|
|
63
|
+
*
|
|
64
|
+
* Fails with `ParseError` if the body is not valid JSON or if the schema
|
|
65
|
+
* rejects the parsed value.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const Body = Schema.Struct({ name: Schema.String });
|
|
70
|
+
* const { name } = yield* decodeJson(Body);
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare const decodeJson: <O>(schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>) => Effect.Effect<O, ParseError, SvelteKitRequestEvent>;
|
|
74
|
+
/**
|
|
75
|
+
* Parse the request body as `FormData` and decode it against `schema`.
|
|
76
|
+
*
|
|
77
|
+
* The raw value passed to the validator is a plain object; fields with
|
|
78
|
+
* multiple values (same name) become arrays.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* const Form = Schema.Struct({ email: Schema.String });
|
|
83
|
+
* const { email } = yield* decodeFormData(Form);
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare const decodeFormData: <O>(schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>) => Effect.Effect<O, ParseError, SvelteKitRequestEvent>;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { ParseError, ParseIssue, decodeFormData, decodeJson, decodeParams, decodeSearchParams };
|
|
89
|
+
//# sourceMappingURL=request-parse.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SvelteKitRequestEvent } from "./runtime.js";
|
|
2
|
+
import { toStandardValidator } from "./internal/remote-validators.js";
|
|
3
|
+
import { Data, Effect } from "effect";
|
|
4
|
+
//#region src/request-parse.ts
|
|
5
|
+
/**
|
|
6
|
+
* Raised when a schema validator rejects the request data.
|
|
7
|
+
*
|
|
8
|
+
* Carry the raw `issues` array from the Standard Schema validator so callers
|
|
9
|
+
* can pattern-match on specific field failures.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { ParseError } from '@thomasfosterau/effect-sveltekit/server';
|
|
14
|
+
* import { Effect } from 'effect';
|
|
15
|
+
*
|
|
16
|
+
* const result = yield* decodeJson(MySchema).pipe(
|
|
17
|
+
* Effect.catchTag("ParseError", (e) => Effect.succeed({ fallback: true })),
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
var ParseError = class extends Data.TaggedError("ParseError") {};
|
|
22
|
+
/** Validate `value` against `schema` and return the typed output or fail. */
|
|
23
|
+
const validate = (schema, value) => Effect.callback((resume) => {
|
|
24
|
+
const resultOrPromise = schema["~standard"].validate(value);
|
|
25
|
+
const settle = (result) => {
|
|
26
|
+
if ("issues" in result && result.issues !== void 0) resume(Effect.fail(new ParseError({ issues: result.issues })));
|
|
27
|
+
else resume(Effect.succeed(result.value));
|
|
28
|
+
};
|
|
29
|
+
if (resultOrPromise instanceof Promise) resultOrPromise.then(settle, (err) => {
|
|
30
|
+
resume(Effect.fail(new ParseError({ issues: [{ message: String(err) }] })));
|
|
31
|
+
});
|
|
32
|
+
else settle(resultOrPromise);
|
|
33
|
+
});
|
|
34
|
+
/** Normalise an Effect Schema or Standard Schema V1 to Standard Schema V1. */
|
|
35
|
+
const normalise = (schema) => toStandardValidator(schema);
|
|
36
|
+
/**
|
|
37
|
+
* Decode the route `params` object against `schema`.
|
|
38
|
+
*
|
|
39
|
+
* `params` is a plain `Record<string, string>` from the matched URL segments;
|
|
40
|
+
* the schema is free to coerce values (e.g. `Schema.NumberFromString`).
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const Params = Schema.Struct({ id: Schema.String });
|
|
45
|
+
* const { id } = yield* decodeParams(Params);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
const decodeParams = (schema) => Effect.gen(function* () {
|
|
49
|
+
const event = yield* SvelteKitRequestEvent;
|
|
50
|
+
return yield* validate(normalise(schema), event.params);
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* Decode the URL search / query parameters against `schema`.
|
|
54
|
+
*
|
|
55
|
+
* The raw value passed to the validator is a plain object built from
|
|
56
|
+
* `event.url.searchParams` (multiple values for the same key become an array).
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const Query = Schema.Struct({ page: Schema.NumberFromString });
|
|
61
|
+
* const { page } = yield* decodeSearchParams(Query);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
const decodeSearchParams = (schema) => Effect.gen(function* () {
|
|
65
|
+
const event = yield* SvelteKitRequestEvent;
|
|
66
|
+
const raw = {};
|
|
67
|
+
for (const key of event.url.searchParams.keys()) {
|
|
68
|
+
const values = event.url.searchParams.getAll(key);
|
|
69
|
+
raw[key] = values.length === 1 ? values[0] : values;
|
|
70
|
+
}
|
|
71
|
+
return yield* validate(normalise(schema), raw);
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Parse the request body as JSON and decode it against `schema`.
|
|
75
|
+
*
|
|
76
|
+
* Fails with `ParseError` if the body is not valid JSON or if the schema
|
|
77
|
+
* rejects the parsed value.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* const Body = Schema.Struct({ name: Schema.String });
|
|
82
|
+
* const { name } = yield* decodeJson(Body);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
const decodeJson = (schema) => Effect.gen(function* () {
|
|
86
|
+
const event = yield* SvelteKitRequestEvent;
|
|
87
|
+
const json = yield* Effect.tryPromise({
|
|
88
|
+
try: () => event.request.json(),
|
|
89
|
+
catch: (err) => new ParseError({ issues: [{ message: `Invalid JSON: ${String(err)}` }] })
|
|
90
|
+
});
|
|
91
|
+
return yield* validate(normalise(schema), json);
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Parse the request body as `FormData` and decode it against `schema`.
|
|
95
|
+
*
|
|
96
|
+
* The raw value passed to the validator is a plain object; fields with
|
|
97
|
+
* multiple values (same name) become arrays.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const Form = Schema.Struct({ email: Schema.String });
|
|
102
|
+
* const { email } = yield* decodeFormData(Form);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
const decodeFormData = (schema) => Effect.gen(function* () {
|
|
106
|
+
const event = yield* SvelteKitRequestEvent;
|
|
107
|
+
const formData = yield* Effect.tryPromise({
|
|
108
|
+
try: () => event.request.formData(),
|
|
109
|
+
catch: (err) => new ParseError({ issues: [{ message: `Failed to read form data: ${String(err)}` }] })
|
|
110
|
+
});
|
|
111
|
+
const raw = {};
|
|
112
|
+
for (const key of formData.keys()) {
|
|
113
|
+
const values = formData.getAll(key).map(String);
|
|
114
|
+
raw[key] = values.length === 1 ? values[0] : values;
|
|
115
|
+
}
|
|
116
|
+
return yield* validate(normalise(schema), raw);
|
|
117
|
+
});
|
|
118
|
+
//#endregion
|
|
119
|
+
export { ParseError, decodeFormData, decodeJson, decodeParams, decodeSearchParams };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=request-parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-parse.js","names":[],"sources":["../src/request-parse.ts"],"sourcesContent":["/**\n * Schema-driven request parsing utilities for SvelteKit server handlers.\n *\n * Each helper reads a part of the current request (route params, search params,\n * JSON body, or form data), validates it against a schema (Effect `Schema` or\n * any Standard Schema V1 validator), and returns the typed output as an\n * `Effect`. A validation failure surfaces as {@link ParseError}.\n *\n * All four utilities require `SvelteKitRequestEvent` in the environment so they\n * compose naturally inside `serverLoad` / `action` / `endpoint` handlers.\n *\n * @example\n * ```ts\n * import { serverLoad } from '@thomasfosterau/effect-sveltekit/server';\n * import { decodeParams, decodeSearchParams } from '@thomasfosterau/effect-sveltekit/server';\n * import { Schema } from 'effect';\n *\n * const Params = Schema.Struct({ id: Schema.String });\n * const Query = Schema.Struct({ page: Schema.NumberFromString });\n *\n * export const load = serverLoad(() =>\n * Effect.gen(function* () {\n * const { id } = yield* decodeParams(Params);\n * const { page } = yield* decodeSearchParams(Query);\n * return { id, page };\n * }),\n * );\n * ```\n */\n\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { Data, Effect, Schema } from \"effect\";\nimport { SvelteKitRequestEvent } from \"./runtime.js\";\nimport { toStandardValidator } from \"./internal/remote-validators.js\";\n\n// ---------------------------------------------------------------------------\n// ParseError\n// ---------------------------------------------------------------------------\n\n/** A single issue reported by a Standard Schema validator. */\nexport interface ParseIssue {\n readonly message: string;\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;\n}\n\n/**\n * Raised when a schema validator rejects the request data.\n *\n * Carry the raw `issues` array from the Standard Schema validator so callers\n * can pattern-match on specific field failures.\n *\n * @example\n * ```ts\n * import { ParseError } from '@thomasfosterau/effect-sveltekit/server';\n * import { Effect } from 'effect';\n *\n * const result = yield* decodeJson(MySchema).pipe(\n * Effect.catchTag(\"ParseError\", (e) => Effect.succeed({ fallback: true })),\n * );\n * ```\n */\nexport class ParseError extends Data.TaggedError(\"ParseError\")<{\n readonly issues: ReadonlyArray<ParseIssue>;\n}> {}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Validate `value` against `schema` and return the typed output or fail. */\nconst validate = <O>(\n schema: StandardSchemaV1<unknown, O>,\n value: unknown,\n): Effect.Effect<O, ParseError> =>\n Effect.callback<O, ParseError>((resume) => {\n const resultOrPromise = schema[\"~standard\"].validate(value);\n const settle = (result: StandardSchemaV1.Result<O>) => {\n if (\"issues\" in result && result.issues !== undefined) {\n resume(Effect.fail(new ParseError({ issues: result.issues as ReadonlyArray<ParseIssue> })));\n } else {\n resume(Effect.succeed((result as StandardSchemaV1.SuccessResult<O>).value));\n }\n };\n if (resultOrPromise instanceof Promise) {\n resultOrPromise.then(settle, (err: unknown) => {\n resume(Effect.fail(new ParseError({ issues: [{ message: String(err) }] })));\n });\n } else {\n settle(resultOrPromise);\n }\n });\n\n/** Normalise an Effect Schema or Standard Schema V1 to Standard Schema V1. */\nconst normalise = <O>(schema: unknown): StandardSchemaV1<unknown, O> =>\n toStandardValidator(schema) as StandardSchemaV1<unknown, O>;\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Decode the route `params` object against `schema`.\n *\n * `params` is a plain `Record<string, string>` from the matched URL segments;\n * the schema is free to coerce values (e.g. `Schema.NumberFromString`).\n *\n * @example\n * ```ts\n * const Params = Schema.Struct({ id: Schema.String });\n * const { id } = yield* decodeParams(Params);\n * ```\n */\nexport const decodeParams = <O>(\n schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>,\n): Effect.Effect<O, ParseError, SvelteKitRequestEvent> =>\n Effect.gen(function* () {\n const event = yield* SvelteKitRequestEvent;\n return yield* validate(normalise<O>(schema), event.params);\n });\n\n/**\n * Decode the URL search / query parameters against `schema`.\n *\n * The raw value passed to the validator is a plain object built from\n * `event.url.searchParams` (multiple values for the same key become an array).\n *\n * @example\n * ```ts\n * const Query = Schema.Struct({ page: Schema.NumberFromString });\n * const { page } = yield* decodeSearchParams(Query);\n * ```\n */\nexport const decodeSearchParams = <O>(\n schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>,\n): Effect.Effect<O, ParseError, SvelteKitRequestEvent> =>\n Effect.gen(function* () {\n const event = yield* SvelteKitRequestEvent;\n const raw: Record<string, string | string[]> = {};\n for (const key of event.url.searchParams.keys()) {\n const values = event.url.searchParams.getAll(key);\n raw[key] = values.length === 1 ? values[0]! : values;\n }\n return yield* validate(normalise<O>(schema), raw);\n });\n\n/**\n * Parse the request body as JSON and decode it against `schema`.\n *\n * Fails with `ParseError` if the body is not valid JSON or if the schema\n * rejects the parsed value.\n *\n * @example\n * ```ts\n * const Body = Schema.Struct({ name: Schema.String });\n * const { name } = yield* decodeJson(Body);\n * ```\n */\nexport const decodeJson = <O>(\n schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>,\n): Effect.Effect<O, ParseError, SvelteKitRequestEvent> =>\n Effect.gen(function* () {\n const event = yield* SvelteKitRequestEvent;\n const json = yield* Effect.tryPromise({\n try: () => event.request.json() as Promise<unknown>,\n catch: (err) => new ParseError({ issues: [{ message: `Invalid JSON: ${String(err)}` }] }),\n });\n return yield* validate(normalise<O>(schema), json);\n });\n\n/**\n * Parse the request body as `FormData` and decode it against `schema`.\n *\n * The raw value passed to the validator is a plain object; fields with\n * multiple values (same name) become arrays.\n *\n * @example\n * ```ts\n * const Form = Schema.Struct({ email: Schema.String });\n * const { email } = yield* decodeFormData(Form);\n * ```\n */\nexport const decodeFormData = <O>(\n schema: Schema.Schema<O> | StandardSchemaV1<unknown, O>,\n): Effect.Effect<O, ParseError, SvelteKitRequestEvent> =>\n Effect.gen(function* () {\n const event = yield* SvelteKitRequestEvent;\n const formData = yield* Effect.tryPromise({\n try: () => event.request.formData(),\n catch: (err) =>\n new ParseError({ issues: [{ message: `Failed to read form data: ${String(err)}` }] }),\n });\n const raw: Record<string, string | string[]> = {};\n for (const key of formData.keys()) {\n const values = formData.getAll(key).map(String);\n raw[key] = values.length === 1 ? values[0]! : values;\n }\n return yield* validate(normalise<O>(schema), raw);\n });\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6DA,IAAa,aAAb,cAAgC,KAAK,YAAY,YAAY,EAE1D,CAAC;;AAOJ,MAAM,YACJ,QACA,UAEA,OAAO,UAAyB,WAAW;CACzC,MAAM,kBAAkB,OAAO,aAAa,SAAS,KAAK;CAC1D,MAAM,UAAU,WAAuC;EACrD,IAAI,YAAY,UAAU,OAAO,WAAW,KAAA,GAC1C,OAAO,OAAO,KAAK,IAAI,WAAW,EAAE,QAAQ,OAAO,OAAoC,CAAC,CAAC,CAAC;OAE1F,OAAO,OAAO,QAAS,OAA6C,KAAK,CAAC;CAE9E;CACA,IAAI,2BAA2B,SAC7B,gBAAgB,KAAK,SAAS,QAAiB;EAC7C,OAAO,OAAO,KAAK,IAAI,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS,OAAO,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5E,CAAC;MAED,OAAO,eAAe;AAE1B,CAAC;;AAGH,MAAM,aAAgB,WACpB,oBAAoB,MAAM;;;;;;;;;;;;;AAkB5B,MAAa,gBACX,WAEA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,OAAO;CACrB,OAAO,OAAO,SAAS,UAAa,MAAM,GAAG,MAAM,MAAM;AAC3D,CAAC;;;;;;;;;;;;;AAcH,MAAa,sBACX,WAEA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,OAAO;CACrB,MAAM,MAAyC,CAAC;CAChD,KAAK,MAAM,OAAO,MAAM,IAAI,aAAa,KAAK,GAAG;EAC/C,MAAM,SAAS,MAAM,IAAI,aAAa,OAAO,GAAG;EAChD,IAAI,OAAO,OAAO,WAAW,IAAI,OAAO,KAAM;CAChD;CACA,OAAO,OAAO,SAAS,UAAa,MAAM,GAAG,GAAG;AAClD,CAAC;;;;;;;;;;;;;AAcH,MAAa,cACX,WAEA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,OAAO;CACrB,MAAM,OAAO,OAAO,OAAO,WAAW;EACpC,WAAW,MAAM,QAAQ,KAAK;EAC9B,QAAQ,QAAQ,IAAI,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS,iBAAiB,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;CAC1F,CAAC;CACD,OAAO,OAAO,SAAS,UAAa,MAAM,GAAG,IAAI;AACnD,CAAC;;;;;;;;;;;;;AAcH,MAAa,kBACX,WAEA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,OAAO;CACrB,MAAM,WAAW,OAAO,OAAO,WAAW;EACxC,WAAW,MAAM,QAAQ,SAAS;EAClC,QAAQ,QACN,IAAI,WAAW,EAAE,QAAQ,CAAC,EAAE,SAAS,6BAA6B,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;CACxF,CAAC;CACD,MAAM,MAAyC,CAAC;CAChD,KAAK,MAAM,OAAO,SAAS,KAAK,GAAG;EACjC,MAAM,SAAS,SAAS,OAAO,GAAG,EAAE,IAAI,MAAM;EAC9C,IAAI,OAAO,OAAO,WAAW,IAAI,OAAO,KAAM;CAChD;CACA,OAAO,OAAO,SAAS,UAAa,MAAM,GAAG,GAAG;AAClD,CAAC"}
|
package/dist/request-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SvelteKit } from "./kit.js";
|
|
2
2
|
import { Cause, Effect, Exit, ManagedRuntime } from "effect";
|
|
3
3
|
import { isHttpError, isRedirect } from "@sveltejs/kit";
|
|
4
4
|
//#region src/request-runtime.ts
|
|
@@ -140,9 +140,7 @@ const RequestRuntime = {
|
|
|
140
140
|
* ```
|
|
141
141
|
*/
|
|
142
142
|
const makeRuntimeAdapter = (provider, opts) => {
|
|
143
|
-
const translateError = opts?.translateError ??
|
|
144
|
-
throw causeToThrowable(cause);
|
|
145
|
-
});
|
|
143
|
+
const translateError = opts?.translateError ?? SvelteKit.translate;
|
|
146
144
|
const runSettle = async (event, effect) => {
|
|
147
145
|
const exit = await provider.run(event).runPromiseExit(effect);
|
|
148
146
|
try {
|
|
@@ -152,9 +150,9 @@ const makeRuntimeAdapter = (provider, opts) => {
|
|
|
152
150
|
provider.disposeAfterResponse?.(event);
|
|
153
151
|
}
|
|
154
152
|
};
|
|
155
|
-
const action = (fn) => (event) => runSettle(event, fn(event));
|
|
153
|
+
const action = (fn) => (event) => runSettle(event, fn(event).pipe(Effect.withSpan(`action ${event.route.id ?? "(unknown)"}`, { kind: "server" })));
|
|
156
154
|
return {
|
|
157
|
-
serverLoad: (fn) => (event) => runSettle(event, fn(event)),
|
|
155
|
+
serverLoad: (fn) => (event) => runSettle(event, fn(event).pipe(Effect.withSpan(`serverLoad ${event.route.id ?? "(unknown)"}`, { kind: "server" }))),
|
|
158
156
|
action,
|
|
159
157
|
actions: (map) => {
|
|
160
158
|
const wrapped = {};
|
|
@@ -162,18 +160,21 @@ const makeRuntimeAdapter = (provider, opts) => {
|
|
|
162
160
|
return wrapped;
|
|
163
161
|
},
|
|
164
162
|
endpoint: (fn, options) => async (event) => {
|
|
165
|
-
const
|
|
163
|
+
const effect = fn(event).pipe(Effect.withSpan(`${event.request.method} ${event.route.id ?? "(unknown)"}`, { kind: "server" }));
|
|
164
|
+
const exit = await provider.run(event).runPromiseExit(effect);
|
|
166
165
|
try {
|
|
167
166
|
if (Exit.isSuccess(exit)) return exit.value;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
try {
|
|
168
|
+
return translateError(exit.cause);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (isRedirect(err) || isHttpError(err)) throw err;
|
|
171
|
+
console.error("Endpoint error:", Cause.pretty(exit.cause));
|
|
172
|
+
const message = options?.exposeErrors ? Cause.pretty(exit.cause) : "Internal server error";
|
|
173
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
174
|
+
status: 500,
|
|
175
|
+
headers: { "Content-Type": "application/json" }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
177
178
|
} finally {
|
|
178
179
|
provider.disposeAfterResponse?.(event);
|
|
179
180
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-runtime.js","names":[],"sources":["../src/request-runtime.ts"],"sourcesContent":["/**\n * A general, Effect-idiomatic way to provide the per-request execution context\n * for SvelteKit server wrappers.\n *\n * Where {@link makeServerRuntime} bakes in \"the package owns a runtime, built\n * once per process,\" this module factors out the single decision *how to obtain\n * the execution context for this request* behind one open abstraction — a\n * {@link RequestRuntimeProvider}. Existing models (a build-once\n * `ManagedRuntime`, a layer built once), a consumer-owned runtime resolved from\n * `event.locals`, and a package-built per-request runtime are all just\n * {@link RequestRuntime} constructors of the same provider, not separate code\n * paths.\n *\n * {@link makeRuntimeAdapter} binds the `serverLoad` / `action` / `actions` /\n * `endpoint` wrappers to a provider (and an optional boundary error\n * translator), so the handler effect's requirement channel `R` is the\n * *provider's* services — your own application catalogue — rather than the fixed\n * {@link SvelteKitServices}.\n *\n * @example\n * ```ts\n * // The consumer owns a per-request ManagedRuntime on event.locals.runtime\n * // (rebuilt mid-request); the package adopts it, resolved at call time.\n * import { makeRuntimeAdapter, RequestRuntime } from \"@thomasfosterau/effect-sveltekit/server\";\n *\n * const { serverLoad, action, actions, endpoint } = makeRuntimeAdapter(\n * RequestRuntime.fromLocals((event) => event.locals.runtime),\n * { translateError: toSvelteKit },\n * );\n *\n * export const load = serverLoad((event) =>\n * Effect.gen(function* () {\n * const db = yield* Database; // app service from locals.runtime\n * return { items: yield* db.list() };\n * }),\n * );\n * ```\n */\n\nimport {\n isHttpError,\n isRedirect,\n type Action,\n type Actions,\n type RequestEvent,\n type RequestHandler,\n type ServerLoad,\n} from \"@sveltejs/kit\";\nimport { Cause, Context, Effect, Exit, type Fiber, Layer, ManagedRuntime } from \"effect\";\nimport type { SvelteRouteParams } from \"./internal/route-params.js\";\nimport { causeToThrowable } from \"./internal/error.js\";\n\n/**\n * The runners a {@link RequestRuntimeProvider} resolves for a single request:\n * a way to run an `Effect<A, unknown, R>` that requires the provider's services\n * `R`. The error channel is `unknown` because a boundary translator (see\n * {@link makeRuntimeAdapter}) decides how failures surface.\n */\nexport interface RequestRuntimeRunners<in R> {\n /** Run the request's effect, keeping its `Exit` (never rejects). */\n readonly runPromiseExit: <A>(\n effect: Effect.Effect<A, unknown, R>,\n ) => Promise<Exit.Exit<A, unknown>>;\n /** Fork the request's effect, returning the running fiber. */\n readonly runFork: <A>(effect: Effect.Effect<A, unknown, R>) => Fiber.Fiber<A, unknown>;\n}\n\n/**\n * How to execute a request's effects. Resolved per request, at call time, so a\n * runtime the consumer rebuilds mid-request is honoured automatically.\n *\n * Implementations own the scope/lifecycle of whatever they build. `R` is the\n * set of services the handler effect may require; `E` is the layer construction\n * error type (informational — failures surface through the translator).\n */\nexport interface RequestRuntimeProvider<R, E = never> {\n /** Resolve the runners for a request. Consulted per call, never cached. */\n readonly run: (event: RequestEvent) => RequestRuntimeRunners<R>;\n /**\n * Optional teardown to run after the response is produced. Called by\n * {@link makeRuntimeAdapter} once the handler has settled — e.g. to route a\n * per-request resource's disposal through `ctx.waitUntil` on Workers.\n */\n readonly disposeAfterResponse?: (event: RequestEvent) => void;\n /** Phantom marker so `E` participates in inference. */\n readonly _E?: (_: never) => E;\n}\n\n/** Runners backed by an existing `ManagedRuntime`. */\nconst runnersFromManagedRuntime = <R>(\n rt: ManagedRuntime.ManagedRuntime<R, unknown>,\n): RequestRuntimeRunners<R> => ({\n runPromiseExit: (effect) => rt.runPromiseExit(effect) as Promise<Exit.Exit<never, unknown>>,\n runFork: (effect) => rt.runFork(effect) as Fiber.Fiber<never, unknown>,\n});\n\n/** Runners backed by a built service `Context` (run on the default scheduler). */\nconst runnersFromContext = <R>(context: Context.Context<R>): RequestRuntimeRunners<R> => ({\n runPromiseExit: (effect) => Effect.runPromiseExitWith(context)(effect),\n runFork: (effect) => Effect.runForkWith(context)(effect),\n});\n\n/** Runners for a target that is either a `ManagedRuntime` or a `Context`. */\nconst runnersFor = <R>(\n target: ManagedRuntime.ManagedRuntime<R, unknown> | Context.Context<R>,\n): RequestRuntimeRunners<R> =>\n ManagedRuntime.isManagedRuntime(target)\n ? runnersFromManagedRuntime(target as ManagedRuntime.ManagedRuntime<R, unknown>)\n : runnersFromContext(target as Context.Context<R>);\n\n/** Options for {@link RequestRuntime.fromLayerPerRequest}. */\nexport interface FromLayerPerRequestOptions {\n /**\n * Defer teardown of the per-request runtime to after the response is\n * produced, instead of awaiting `dispose()` inline. On Workers, route it\n * through `ctx.waitUntil` so disposal does not fight response streaming or\n * socket pinning:\n *\n * ```ts\n * { afterResponse: (event, dispose) => event.platform!.ctx.waitUntil(dispose()) }\n * ```\n */\n readonly afterResponse?: (event: RequestEvent, dispose: () => Promise<void>) => void;\n}\n\n/**\n * Constructors for {@link RequestRuntimeProvider}. Each expresses one way to\n * obtain a request's execution context; the wrappers don't care which.\n */\nexport const RequestRuntime = {\n /**\n * Adopt a `ManagedRuntime` the consumer already built (the build-once model,\n * expressed as a provider).\n */\n fromManagedRuntime: <R, E>(\n rt: ManagedRuntime.ManagedRuntime<R, E>,\n ): RequestRuntimeProvider<R, E> => {\n const runners = runnersFromManagedRuntime(rt as ManagedRuntime.ManagedRuntime<R, unknown>);\n return { run: () => runners };\n },\n\n /**\n * Let the package build a single `ManagedRuntime` from a layer, lazily on\n * first use and shared across requests (the existing build-once-per-process\n * model).\n */\n fromLayer: <R, E>(layer: Layer.Layer<R, E>): RequestRuntimeProvider<R, E> => {\n let runners: RequestRuntimeRunners<R> | undefined;\n return {\n run: () =>\n (runners ??= runnersFromManagedRuntime(\n ManagedRuntime.make(layer) as ManagedRuntime.ManagedRuntime<R, unknown>,\n )),\n };\n },\n\n /**\n * Adopt a runtime the consumer stashed on the request (e.g. on\n * `event.locals`). Resolved at call time, so a runtime rebuilt mid-request is\n * honoured automatically. The getter may return a `ManagedRuntime` or a\n * built service `Context`.\n */\n fromLocals: <R>(\n get: (event: RequestEvent) => ManagedRuntime.ManagedRuntime<R, unknown> | Context.Context<R>,\n ): RequestRuntimeProvider<R> => ({\n run: (event) => runnersFor(get(event)),\n }),\n\n /**\n * Let the package build *and dispose* a `ManagedRuntime` per request from a\n * layer factory. Each handler invocation builds its own runtime from\n * `layer(event)` and disposes it once the effect settles — so a per-request\n * resource (a DB pool) dies with the request. Use `opts.afterResponse` to\n * defer disposal to after the response (Workers `ctx.waitUntil`).\n *\n * For sharing one runtime across every load/action of a request, prefer\n * {@link RequestRuntime.fromLocals} with a runtime the consumer builds in\n * `hooks.server.ts`.\n */\n fromLayerPerRequest: <R, E>(\n layer: (event: RequestEvent) => Layer.Layer<R, E>,\n opts?: FromLayerPerRequestOptions,\n ): RequestRuntimeProvider<R, E> => ({\n run: (event) => {\n const dispose = (rt: ManagedRuntime.ManagedRuntime<R, E>) => () => rt.dispose();\n const teardown = (rt: ManagedRuntime.ManagedRuntime<R, E>): Promise<void> => {\n if (opts?.afterResponse) {\n opts.afterResponse(event, dispose(rt));\n return Promise.resolve();\n }\n return rt.dispose();\n };\n return {\n runPromiseExit: async (effect) => {\n const rt = ManagedRuntime.make(layer(event));\n try {\n return (await rt.runPromiseExit(effect)) as Exit.Exit<never, unknown>;\n } finally {\n await teardown(rt);\n }\n },\n runFork: (effect) => {\n const rt = ManagedRuntime.make(layer(event));\n const fiber = rt.runFork(effect) as Fiber.Fiber<never, unknown>;\n fiber.addObserver(() => void teardown(rt));\n return fiber;\n },\n };\n },\n }),\n};\n\n/** A boundary translator: turn a failed `Cause` into a thrown value. */\nexport type ErrorTranslator = (cause: Cause.Cause<unknown>) => never;\n\n/** Options shared by {@link makeRuntimeAdapter}. */\nexport interface RuntimeAdapterOptions {\n /**\n * Translate a failed `Cause` into SvelteKit control flow at the boundary.\n * Called for `serverLoad` / `action` / `actions` failures; it may throw\n * `redirect()` / `error()` (and, for remote forms, call `invalid()`).\n * Defaults to the package's passthrough: SvelteKit's own `redirect()` /\n * `error()` are rethrown, anything else becomes an `EffectCauseError` (a 500).\n */\n readonly translateError?: ErrorTranslator;\n}\n\n/** Options for the adapter's `endpoint` wrapper. */\nexport interface AdapterEndpointOptions {\n /**\n * Include full error details (`Cause.pretty`) in the default 500 JSON body.\n * Ignored when a `translateError` is configured. Defaults to `false`.\n */\n readonly exposeErrors?: boolean;\n}\n\n/** The wrappers produced by {@link makeRuntimeAdapter}, bound to a provider. */\nexport interface RuntimeAdapter<R> {\n readonly serverLoad: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n ParentData extends Record<string, any> = any,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>,\n ) => ServerLoad<Params, ParentData, OutputData, RouteId>;\n readonly action: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>,\n ) => Action<Params, OutputData, RouteId>;\n readonly actions: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n map: Record<\n string,\n (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>\n >,\n ) => Actions<Params, OutputData, RouteId>;\n readonly endpoint: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<Response, unknown, R>,\n options?: AdapterEndpointOptions,\n ) => RequestHandler<Params, RouteId>;\n}\n\n/**\n * Bind the SvelteKit server wrappers to a {@link RequestRuntimeProvider} (and an\n * optional boundary error translator).\n *\n * Unlike the standalone `serverLoad` / `action` / `endpoint`, the handler\n * effect's required services `R` come from the provider — your own application\n * catalogue — not the fixed {@link SvelteKitServices}. The package does not\n * merge `HttpClient` / `SvelteKitRequestEvent` onto this path; add them to your\n * own layer if you want them.\n *\n * @example\n * ```ts\n * // Package owns the runtime per request on Workers, disposed via waitUntil.\n * const { serverLoad } = makeRuntimeAdapter(\n * RequestRuntime.fromLayerPerRequest((event) => AppLayer(event.platform!.env), {\n * afterResponse: (event, dispose) => event.platform!.ctx.waitUntil(dispose()),\n * }),\n * );\n * ```\n */\nexport const makeRuntimeAdapter = <R>(\n provider: RequestRuntimeProvider<R, any>,\n opts?: RuntimeAdapterOptions,\n): RuntimeAdapter<R> => {\n const translateError: ErrorTranslator =\n opts?.translateError ??\n ((cause) => {\n throw causeToThrowable(cause);\n });\n\n const runSettle = async <A>(\n event: RequestEvent,\n effect: Effect.Effect<A, unknown, R>,\n ): Promise<A> => {\n const exit = await provider.run(event).runPromiseExit(effect);\n try {\n if (Exit.isSuccess(exit)) return exit.value;\n return translateError(exit.cause);\n } finally {\n provider.disposeAfterResponse?.(event);\n }\n };\n\n const action: RuntimeAdapter<R>[\"action\"] = (fn) => (event) => runSettle(event, fn(event));\n\n return {\n serverLoad: (fn) => (event) => runSettle(event, fn(event)),\n action,\n actions: (map) => {\n const wrapped: Record<string, Action<any, any, any>> = {};\n for (const [key, fn] of Object.entries(map)) wrapped[key] = action(fn);\n return wrapped as never;\n },\n endpoint: (fn, options) => async (event) => {\n const exit = await provider.run(event as RequestEvent).runPromiseExit(fn(event));\n try {\n if (Exit.isSuccess(exit)) return exit.value;\n // A configured translator owns the whole failure path (control flow,\n // structured error bodies, ...).\n if (opts?.translateError) return opts.translateError(exit.cause);\n // Default: pass SvelteKit control flow through, otherwise a 500.\n const squashed = Cause.squash(exit.cause);\n if (isRedirect(squashed) || isHttpError(squashed)) throw squashed;\n console.error(\"Endpoint error:\", Cause.pretty(exit.cause));\n const message = options?.exposeErrors ? Cause.pretty(exit.cause) : \"Internal server error\";\n return new Response(JSON.stringify({ error: message }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n } finally {\n provider.disposeAfterResponse?.(event as RequestEvent);\n }\n },\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-runtime.js","names":[],"sources":["../src/request-runtime.ts"],"sourcesContent":["/**\n * A general, Effect-idiomatic way to provide the per-request execution context\n * for SvelteKit server wrappers.\n *\n * Where {@link makeServerRuntime} bakes in \"the package owns a runtime, built\n * once per process,\" this module factors out the single decision *how to obtain\n * the execution context for this request* behind one open abstraction — a\n * {@link RequestRuntimeProvider}. Existing models (a build-once\n * `ManagedRuntime`, a layer built once), a consumer-owned runtime resolved from\n * `event.locals`, and a package-built per-request runtime are all just\n * {@link RequestRuntime} constructors of the same provider, not separate code\n * paths.\n *\n * {@link makeRuntimeAdapter} binds the `serverLoad` / `action` / `actions` /\n * `endpoint` wrappers to a provider (and an optional boundary error\n * translator), so the handler effect's requirement channel `R` is the\n * *provider's* services — your own application catalogue — rather than the fixed\n * {@link SvelteKitServices}.\n *\n * @example\n * ```ts\n * // The consumer owns a per-request ManagedRuntime on event.locals.runtime\n * // (rebuilt mid-request); the package adopts it, resolved at call time.\n * import { makeRuntimeAdapter, RequestRuntime } from \"@thomasfosterau/effect-sveltekit/server\";\n *\n * const { serverLoad, action, actions, endpoint } = makeRuntimeAdapter(\n * RequestRuntime.fromLocals((event) => event.locals.runtime),\n * { translateError: toSvelteKit },\n * );\n *\n * export const load = serverLoad((event) =>\n * Effect.gen(function* () {\n * const db = yield* Database; // app service from locals.runtime\n * return { items: yield* db.list() };\n * }),\n * );\n * ```\n */\n\nimport {\n isHttpError,\n isRedirect,\n type Action,\n type Actions,\n type RequestEvent,\n type RequestHandler,\n type ServerLoad,\n} from \"@sveltejs/kit\";\nimport { Cause, Context, Effect, Exit, type Fiber, Layer, ManagedRuntime } from \"effect\";\nimport type { SvelteRouteParams } from \"./internal/route-params.js\";\nimport { SvelteKit } from \"./kit.js\";\n\n/**\n * The runners a {@link RequestRuntimeProvider} resolves for a single request:\n * a way to run an `Effect<A, unknown, R>` that requires the provider's services\n * `R`. The error channel is `unknown` because a boundary translator (see\n * {@link makeRuntimeAdapter}) decides how failures surface.\n */\nexport interface RequestRuntimeRunners<in R> {\n /** Run the request's effect, keeping its `Exit` (never rejects). */\n readonly runPromiseExit: <A>(\n effect: Effect.Effect<A, unknown, R>,\n ) => Promise<Exit.Exit<A, unknown>>;\n /** Fork the request's effect, returning the running fiber. */\n readonly runFork: <A>(effect: Effect.Effect<A, unknown, R>) => Fiber.Fiber<A, unknown>;\n}\n\n/**\n * How to execute a request's effects. Resolved per request, at call time, so a\n * runtime the consumer rebuilds mid-request is honoured automatically.\n *\n * Implementations own the scope/lifecycle of whatever they build. `R` is the\n * set of services the handler effect may require; `E` is the layer construction\n * error type (informational — failures surface through the translator).\n */\nexport interface RequestRuntimeProvider<R, E = never> {\n /** Resolve the runners for a request. Consulted per call, never cached. */\n readonly run: (event: RequestEvent) => RequestRuntimeRunners<R>;\n /**\n * Optional teardown to run after the response is produced. Called by\n * {@link makeRuntimeAdapter} once the handler has settled — e.g. to route a\n * per-request resource's disposal through `ctx.waitUntil` on Workers.\n */\n readonly disposeAfterResponse?: (event: RequestEvent) => void;\n /** Phantom marker so `E` participates in inference. */\n readonly _E?: (_: never) => E;\n}\n\n/** Runners backed by an existing `ManagedRuntime`. */\nconst runnersFromManagedRuntime = <R>(\n rt: ManagedRuntime.ManagedRuntime<R, unknown>,\n): RequestRuntimeRunners<R> => ({\n runPromiseExit: (effect) => rt.runPromiseExit(effect) as Promise<Exit.Exit<never, unknown>>,\n runFork: (effect) => rt.runFork(effect) as Fiber.Fiber<never, unknown>,\n});\n\n/** Runners backed by a built service `Context` (run on the default scheduler). */\nconst runnersFromContext = <R>(context: Context.Context<R>): RequestRuntimeRunners<R> => ({\n runPromiseExit: (effect) => Effect.runPromiseExitWith(context)(effect),\n runFork: (effect) => Effect.runForkWith(context)(effect),\n});\n\n/** Runners for a target that is either a `ManagedRuntime` or a `Context`. */\nconst runnersFor = <R>(\n target: ManagedRuntime.ManagedRuntime<R, unknown> | Context.Context<R>,\n): RequestRuntimeRunners<R> =>\n ManagedRuntime.isManagedRuntime(target)\n ? runnersFromManagedRuntime(target as ManagedRuntime.ManagedRuntime<R, unknown>)\n : runnersFromContext(target as Context.Context<R>);\n\n/** Options for {@link RequestRuntime.fromLayerPerRequest}. */\nexport interface FromLayerPerRequestOptions {\n /**\n * Defer teardown of the per-request runtime to after the response is\n * produced, instead of awaiting `dispose()` inline. On Workers, route it\n * through `ctx.waitUntil` so disposal does not fight response streaming or\n * socket pinning:\n *\n * ```ts\n * { afterResponse: (event, dispose) => event.platform!.ctx.waitUntil(dispose()) }\n * ```\n */\n readonly afterResponse?: (event: RequestEvent, dispose: () => Promise<void>) => void;\n}\n\n/**\n * Constructors for {@link RequestRuntimeProvider}. Each expresses one way to\n * obtain a request's execution context; the wrappers don't care which.\n */\nexport const RequestRuntime = {\n /**\n * Adopt a `ManagedRuntime` the consumer already built (the build-once model,\n * expressed as a provider).\n */\n fromManagedRuntime: <R, E>(\n rt: ManagedRuntime.ManagedRuntime<R, E>,\n ): RequestRuntimeProvider<R, E> => {\n const runners = runnersFromManagedRuntime(rt as ManagedRuntime.ManagedRuntime<R, unknown>);\n return { run: () => runners };\n },\n\n /**\n * Let the package build a single `ManagedRuntime` from a layer, lazily on\n * first use and shared across requests (the existing build-once-per-process\n * model).\n */\n fromLayer: <R, E>(layer: Layer.Layer<R, E>): RequestRuntimeProvider<R, E> => {\n let runners: RequestRuntimeRunners<R> | undefined;\n return {\n run: () =>\n (runners ??= runnersFromManagedRuntime(\n ManagedRuntime.make(layer) as ManagedRuntime.ManagedRuntime<R, unknown>,\n )),\n };\n },\n\n /**\n * Adopt a runtime the consumer stashed on the request (e.g. on\n * `event.locals`). Resolved at call time, so a runtime rebuilt mid-request is\n * honoured automatically. The getter may return a `ManagedRuntime` or a\n * built service `Context`.\n */\n fromLocals: <R>(\n get: (event: RequestEvent) => ManagedRuntime.ManagedRuntime<R, unknown> | Context.Context<R>,\n ): RequestRuntimeProvider<R> => ({\n run: (event) => runnersFor(get(event)),\n }),\n\n /**\n * Let the package build *and dispose* a `ManagedRuntime` per request from a\n * layer factory. Each handler invocation builds its own runtime from\n * `layer(event)` and disposes it once the effect settles — so a per-request\n * resource (a DB pool) dies with the request. Use `opts.afterResponse` to\n * defer disposal to after the response (Workers `ctx.waitUntil`).\n *\n * For sharing one runtime across every load/action of a request, prefer\n * {@link RequestRuntime.fromLocals} with a runtime the consumer builds in\n * `hooks.server.ts`.\n */\n fromLayerPerRequest: <R, E>(\n layer: (event: RequestEvent) => Layer.Layer<R, E>,\n opts?: FromLayerPerRequestOptions,\n ): RequestRuntimeProvider<R, E> => ({\n run: (event) => {\n const dispose = (rt: ManagedRuntime.ManagedRuntime<R, E>) => () => rt.dispose();\n const teardown = (rt: ManagedRuntime.ManagedRuntime<R, E>): Promise<void> => {\n if (opts?.afterResponse) {\n opts.afterResponse(event, dispose(rt));\n return Promise.resolve();\n }\n return rt.dispose();\n };\n return {\n runPromiseExit: async (effect) => {\n const rt = ManagedRuntime.make(layer(event));\n try {\n return (await rt.runPromiseExit(effect)) as Exit.Exit<never, unknown>;\n } finally {\n await teardown(rt);\n }\n },\n runFork: (effect) => {\n const rt = ManagedRuntime.make(layer(event));\n const fiber = rt.runFork(effect) as Fiber.Fiber<never, unknown>;\n fiber.addObserver(() => void teardown(rt));\n return fiber;\n },\n };\n },\n }),\n};\n\n/** A boundary translator: turn a failed `Cause` into a thrown value. */\nexport type ErrorTranslator = (cause: Cause.Cause<unknown>) => never;\n\n/** Options shared by {@link makeRuntimeAdapter}. */\nexport interface RuntimeAdapterOptions {\n /**\n * Translate a failed `Cause` into SvelteKit control flow at the boundary.\n * Called for `serverLoad` / `action` / `actions` failures; it may throw\n * `redirect()` / `error()` (and, for remote forms, call `invalid()`).\n * Defaults to the package's passthrough: SvelteKit's own `redirect()` /\n * `error()` are rethrown, anything else becomes an `EffectCauseError` (a 500).\n */\n readonly translateError?: ErrorTranslator;\n}\n\n/** Options for the adapter's `endpoint` wrapper. */\nexport interface AdapterEndpointOptions {\n /**\n * Include full error details (`Cause.pretty`) in the default 500 JSON body.\n * Ignored when a `translateError` is configured. Defaults to `false`.\n */\n readonly exposeErrors?: boolean;\n}\n\n/** The wrappers produced by {@link makeRuntimeAdapter}, bound to a provider. */\nexport interface RuntimeAdapter<R> {\n readonly serverLoad: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n ParentData extends Record<string, any> = any,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>,\n ) => ServerLoad<Params, ParentData, OutputData, RouteId>;\n readonly action: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>,\n ) => Action<Params, OutputData, RouteId>;\n readonly actions: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n OutputData extends Record<string, any> | void = any,\n RouteId extends string | null = any,\n >(\n map: Record<\n string,\n (event: RequestEvent<Params, RouteId>) => Effect.Effect<OutputData, unknown, R>\n >,\n ) => Actions<Params, OutputData, RouteId>;\n readonly endpoint: <\n Params extends SvelteRouteParams = SvelteRouteParams,\n RouteId extends string | null = any,\n >(\n fn: (event: RequestEvent<Params, RouteId>) => Effect.Effect<Response, unknown, R>,\n options?: AdapterEndpointOptions,\n ) => RequestHandler<Params, RouteId>;\n}\n\n/**\n * Bind the SvelteKit server wrappers to a {@link RequestRuntimeProvider} (and an\n * optional boundary error translator).\n *\n * Unlike the standalone `serverLoad` / `action` / `endpoint`, the handler\n * effect's required services `R` come from the provider — your own application\n * catalogue — not the fixed {@link SvelteKitServices}. The package does not\n * merge `HttpClient` / `SvelteKitRequestEvent` onto this path; add them to your\n * own layer if you want them.\n *\n * @example\n * ```ts\n * // Package owns the runtime per request on Workers, disposed via waitUntil.\n * const { serverLoad } = makeRuntimeAdapter(\n * RequestRuntime.fromLayerPerRequest((event) => AppLayer(event.platform!.env), {\n * afterResponse: (event, dispose) => event.platform!.ctx.waitUntil(dispose()),\n * }),\n * );\n * ```\n */\nexport const makeRuntimeAdapter = <R>(\n provider: RequestRuntimeProvider<R, any>,\n opts?: RuntimeAdapterOptions,\n): RuntimeAdapter<R> => {\n const translateError: ErrorTranslator = opts?.translateError ?? SvelteKit.translate;\n\n const runSettle = async <A>(\n event: RequestEvent,\n effect: Effect.Effect<A, unknown, R>,\n ): Promise<A> => {\n const exit = await provider.run(event).runPromiseExit(effect);\n try {\n if (Exit.isSuccess(exit)) return exit.value;\n return translateError(exit.cause);\n } finally {\n provider.disposeAfterResponse?.(event);\n }\n };\n\n const action: RuntimeAdapter<R>[\"action\"] = (fn) => (event) =>\n runSettle(\n event,\n fn(event).pipe(\n Effect.withSpan(`action ${event.route.id ?? \"(unknown)\"}`, { kind: \"server\" }),\n ),\n );\n\n return {\n serverLoad: (fn) => (event) =>\n runSettle(\n event,\n fn(event).pipe(\n Effect.withSpan(`serverLoad ${event.route.id ?? \"(unknown)\"}`, { kind: \"server\" }),\n ),\n ),\n action,\n actions: (map) => {\n const wrapped: Record<string, Action<any, any, any>> = {};\n for (const [key, fn] of Object.entries(map)) wrapped[key] = action(fn);\n return wrapped as never;\n },\n endpoint: (fn, options) => async (event) => {\n const effect = fn(event).pipe(\n Effect.withSpan(\n `${(event as RequestEvent).request.method} ${(event as RequestEvent).route.id ?? \"(unknown)\"}`,\n { kind: \"server\" },\n ),\n );\n const exit = await provider.run(event as RequestEvent).runPromiseExit(effect);\n try {\n if (Exit.isSuccess(exit)) return exit.value;\n // Run the translator (default: SvelteKit.translate). SvelteKit\n // control-flow objects (redirect, error, KitRedirect, KitError) are\n // thrown so SvelteKit can handle them. Anything unrecognised is wrapped\n // in an EffectCauseError and caught below to produce a 500 Response.\n try {\n return translateError(exit.cause);\n } catch (err) {\n if (isRedirect(err) || isHttpError(err)) throw err;\n console.error(\"Endpoint error:\", Cause.pretty(exit.cause));\n const message = options?.exposeErrors\n ? Cause.pretty(exit.cause)\n : \"Internal server error\";\n return new Response(JSON.stringify({ error: message }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n } finally {\n provider.disposeAfterResponse?.(event as RequestEvent);\n }\n },\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SvelteKitRequestEvent } from "./runtime.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import { Cookies, RequestEvent } from "@sveltejs/kit";
|
|
4
|
+
|
|
5
|
+
//#region src/request-services.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The Effect-native interface exposed by {@link SvelteKitCookies}. Each method
|
|
8
|
+
* mirrors the corresponding `Cookies` method from `@sveltejs/kit` but returns
|
|
9
|
+
* an `Effect` rather than a raw value or `void`.
|
|
10
|
+
*/
|
|
11
|
+
interface SvelteKitCookiesInterface {
|
|
12
|
+
/** Get a cookie by name. Returns `undefined` when the cookie is absent. */
|
|
13
|
+
readonly get: (...args: Parameters<Cookies["get"]>) => Effect.Effect<ReturnType<Cookies["get"]>>;
|
|
14
|
+
/** Get all cookies as `{ name, value }` pairs. */
|
|
15
|
+
readonly getAll: (...args: Parameters<Cookies["getAll"]>) => Effect.Effect<ReturnType<Cookies["getAll"]>>;
|
|
16
|
+
/** Set a cookie (adds a `set-cookie` response header). */
|
|
17
|
+
readonly set: (...args: Parameters<Cookies["set"]>) => Effect.Effect<void>;
|
|
18
|
+
/** Delete a cookie by setting it to an empty value with a past expiry. */
|
|
19
|
+
readonly delete: (...args: Parameters<Cookies["delete"]>) => Effect.Effect<void>;
|
|
20
|
+
/** Serialize a cookie into a `Set-Cookie` string without adding it to the response. */
|
|
21
|
+
readonly serialize: (...args: Parameters<Cookies["serialize"]>) => Effect.Effect<ReturnType<Cookies["serialize"]>>;
|
|
22
|
+
}
|
|
23
|
+
declare const SvelteKitCookies_base: Context.ServiceClass<SvelteKitCookies, "@thomasfosterau/effect-sveltekit/SvelteKitCookies", SvelteKitCookiesInterface>;
|
|
24
|
+
/**
|
|
25
|
+
* An Effect service wrapping the `RequestEvent.cookies` API.
|
|
26
|
+
*
|
|
27
|
+
* Available automatically in every server handler effect (it is part of
|
|
28
|
+
* {@link SvelteKitServices}). Use `yield* SvelteKitCookies` to obtain the
|
|
29
|
+
* service; call its methods (each returns an `Effect`) to read or write
|
|
30
|
+
* cookies.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const session = yield* (yield* SvelteKitCookies).get("session");
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare class SvelteKitCookies extends SvelteKitCookies_base {
|
|
38
|
+
/**
|
|
39
|
+
* Layer that builds the `SvelteKitCookies` service from the current
|
|
40
|
+
* {@link SvelteKitRequestEvent}. Included in the per-request context
|
|
41
|
+
* automatically; expose as a standalone layer for custom setups.
|
|
42
|
+
*/
|
|
43
|
+
static readonly layer: Layer.Layer<SvelteKitCookies, never, SvelteKitRequestEvent>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The Effect-native interface exposed by {@link SvelteKitResponseHeaders}.
|
|
47
|
+
*/
|
|
48
|
+
interface SvelteKitResponseHeadersInterface {
|
|
49
|
+
/**
|
|
50
|
+
* Set one or more response headers. Equivalent to `event.setHeaders(headers)`.
|
|
51
|
+
*
|
|
52
|
+
* Note: You cannot set `set-cookie` headers here — use
|
|
53
|
+
* {@link SvelteKitCookies} instead.
|
|
54
|
+
*/
|
|
55
|
+
readonly set: (headers: Record<string, string>) => Effect.Effect<void>;
|
|
56
|
+
}
|
|
57
|
+
declare const SvelteKitResponseHeaders_base: Context.ServiceClass<SvelteKitResponseHeaders, "@thomasfosterau/effect-sveltekit/SvelteKitResponseHeaders", SvelteKitResponseHeadersInterface>;
|
|
58
|
+
/**
|
|
59
|
+
* An Effect service wrapping the `RequestEvent.setHeaders` API.
|
|
60
|
+
*
|
|
61
|
+
* Available automatically in every server handler effect. Use
|
|
62
|
+
* `yield* SvelteKitResponseHeaders` to obtain the service, then call
|
|
63
|
+
* `headers.set(...)`.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const headers = yield* SvelteKitResponseHeaders;
|
|
68
|
+
* yield* headers.set({ "cache-control": "max-age=60, s-maxage=300" });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare class SvelteKitResponseHeaders extends SvelteKitResponseHeaders_base {
|
|
72
|
+
/**
|
|
73
|
+
* Layer that builds the `SvelteKitResponseHeaders` service from the current
|
|
74
|
+
* {@link SvelteKitRequestEvent}.
|
|
75
|
+
*/
|
|
76
|
+
static readonly layer: Layer.Layer<SvelteKitResponseHeaders, never, SvelteKitRequestEvent>;
|
|
77
|
+
}
|
|
78
|
+
declare const SvelteKitLocals_base: Context.ServiceClass<SvelteKitLocals, "@thomasfosterau/effect-sveltekit/SvelteKitLocals", App.Locals>;
|
|
79
|
+
/**
|
|
80
|
+
* An Effect service that exposes the mutable `App.Locals` object for the
|
|
81
|
+
* current request.
|
|
82
|
+
*
|
|
83
|
+
* `yield* SvelteKitLocals` returns the live `event.locals` reference — the
|
|
84
|
+
* same object mutations in `hooks.server.ts` (e.g. setting `event.locals.user`
|
|
85
|
+
* in a `handle` hook) write to. Mutations made to it inside handler effects
|
|
86
|
+
* propagate back to subsequent hook middleware.
|
|
87
|
+
*
|
|
88
|
+
* Available automatically in every server handler effect.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* // Read a typed local (requires App.Locals augmentation in your app):
|
|
93
|
+
* const { user } = yield* SvelteKitLocals;
|
|
94
|
+
*
|
|
95
|
+
* // Write a local (useful in handle hooks run via makeServerRuntime.handle):
|
|
96
|
+
* const locals = yield* SvelteKitLocals;
|
|
97
|
+
* locals.user = yield* Auth.currentUser;
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
declare class SvelteKitLocals extends SvelteKitLocals_base {
|
|
101
|
+
/**
|
|
102
|
+
* Layer that exposes `event.locals` (the mutable `App.Locals` reference)
|
|
103
|
+
* from the current {@link SvelteKitRequestEvent}.
|
|
104
|
+
*/
|
|
105
|
+
static readonly layer: Layer.Layer<SvelteKitLocals, never, SvelteKitRequestEvent>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A convenience layer that provides all three request services
|
|
109
|
+
* ({@link SvelteKitCookies}, {@link SvelteKitResponseHeaders},
|
|
110
|
+
* {@link SvelteKitLocals}) from the current {@link SvelteKitRequestEvent}.
|
|
111
|
+
*
|
|
112
|
+
* This layer is merged into the per-request context automatically by
|
|
113
|
+
* {@link makeServerRuntime}, the standalone wrappers, and
|
|
114
|
+
* {@link makeRuntimeAdapter}. Expose it here for use in custom setups that
|
|
115
|
+
* already have `SvelteKitRequestEvent` in scope.
|
|
116
|
+
*/
|
|
117
|
+
declare const requestServicesLayer: Layer.Layer<SvelteKitCookies | SvelteKitResponseHeaders | SvelteKitLocals, never, SvelteKitRequestEvent>;
|
|
118
|
+
//#endregion
|
|
119
|
+
export { SvelteKitCookies, SvelteKitCookiesInterface, SvelteKitLocals, SvelteKitResponseHeaders, SvelteKitResponseHeadersInterface, requestServicesLayer };
|
|
120
|
+
//# sourceMappingURL=request-services.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SvelteKitRequestEvent } from "./runtime.js";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
//#region src/request-services.ts
|
|
4
|
+
/**
|
|
5
|
+
* An Effect service wrapping the `RequestEvent.cookies` API.
|
|
6
|
+
*
|
|
7
|
+
* Available automatically in every server handler effect (it is part of
|
|
8
|
+
* {@link SvelteKitServices}). Use `yield* SvelteKitCookies` to obtain the
|
|
9
|
+
* service; call its methods (each returns an `Effect`) to read or write
|
|
10
|
+
* cookies.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const session = yield* (yield* SvelteKitCookies).get("session");
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
var SvelteKitCookies = class SvelteKitCookies extends Context.Service()("@thomasfosterau/effect-sveltekit/SvelteKitCookies") {
|
|
18
|
+
/**
|
|
19
|
+
* Layer that builds the `SvelteKitCookies` service from the current
|
|
20
|
+
* {@link SvelteKitRequestEvent}. Included in the per-request context
|
|
21
|
+
* automatically; expose as a standalone layer for custom setups.
|
|
22
|
+
*/
|
|
23
|
+
static layer = Layer.effect(SvelteKitCookies, Effect.gen(function* () {
|
|
24
|
+
const { cookies } = yield* SvelteKitRequestEvent;
|
|
25
|
+
return {
|
|
26
|
+
get: (...args) => Effect.sync(() => cookies.get(...args)),
|
|
27
|
+
getAll: (...args) => Effect.sync(() => cookies.getAll(...args)),
|
|
28
|
+
set: (...args) => Effect.sync(() => cookies.set(...args)),
|
|
29
|
+
delete: (...args) => Effect.sync(() => cookies.delete(...args)),
|
|
30
|
+
serialize: (...args) => Effect.sync(() => cookies.serialize(...args))
|
|
31
|
+
};
|
|
32
|
+
}));
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* An Effect service wrapping the `RequestEvent.setHeaders` API.
|
|
36
|
+
*
|
|
37
|
+
* Available automatically in every server handler effect. Use
|
|
38
|
+
* `yield* SvelteKitResponseHeaders` to obtain the service, then call
|
|
39
|
+
* `headers.set(...)`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const headers = yield* SvelteKitResponseHeaders;
|
|
44
|
+
* yield* headers.set({ "cache-control": "max-age=60, s-maxage=300" });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
var SvelteKitResponseHeaders = class SvelteKitResponseHeaders extends Context.Service()("@thomasfosterau/effect-sveltekit/SvelteKitResponseHeaders") {
|
|
48
|
+
/**
|
|
49
|
+
* Layer that builds the `SvelteKitResponseHeaders` service from the current
|
|
50
|
+
* {@link SvelteKitRequestEvent}.
|
|
51
|
+
*/
|
|
52
|
+
static layer = Layer.effect(SvelteKitResponseHeaders, Effect.gen(function* () {
|
|
53
|
+
const event = yield* SvelteKitRequestEvent;
|
|
54
|
+
return { set: (headers) => Effect.sync(() => event.setHeaders(headers)) };
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* An Effect service that exposes the mutable `App.Locals` object for the
|
|
59
|
+
* current request.
|
|
60
|
+
*
|
|
61
|
+
* `yield* SvelteKitLocals` returns the live `event.locals` reference — the
|
|
62
|
+
* same object mutations in `hooks.server.ts` (e.g. setting `event.locals.user`
|
|
63
|
+
* in a `handle` hook) write to. Mutations made to it inside handler effects
|
|
64
|
+
* propagate back to subsequent hook middleware.
|
|
65
|
+
*
|
|
66
|
+
* Available automatically in every server handler effect.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // Read a typed local (requires App.Locals augmentation in your app):
|
|
71
|
+
* const { user } = yield* SvelteKitLocals;
|
|
72
|
+
*
|
|
73
|
+
* // Write a local (useful in handle hooks run via makeServerRuntime.handle):
|
|
74
|
+
* const locals = yield* SvelteKitLocals;
|
|
75
|
+
* locals.user = yield* Auth.currentUser;
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
var SvelteKitLocals = class SvelteKitLocals extends Context.Service()("@thomasfosterau/effect-sveltekit/SvelteKitLocals") {
|
|
79
|
+
/**
|
|
80
|
+
* Layer that exposes `event.locals` (the mutable `App.Locals` reference)
|
|
81
|
+
* from the current {@link SvelteKitRequestEvent}.
|
|
82
|
+
*/
|
|
83
|
+
static layer = Layer.effect(SvelteKitLocals, Effect.gen(function* () {
|
|
84
|
+
return (yield* SvelteKitRequestEvent).locals;
|
|
85
|
+
}));
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* A convenience layer that provides all three request services
|
|
89
|
+
* ({@link SvelteKitCookies}, {@link SvelteKitResponseHeaders},
|
|
90
|
+
* {@link SvelteKitLocals}) from the current {@link SvelteKitRequestEvent}.
|
|
91
|
+
*
|
|
92
|
+
* This layer is merged into the per-request context automatically by
|
|
93
|
+
* {@link makeServerRuntime}, the standalone wrappers, and
|
|
94
|
+
* {@link makeRuntimeAdapter}. Expose it here for use in custom setups that
|
|
95
|
+
* already have `SvelteKitRequestEvent` in scope.
|
|
96
|
+
*/
|
|
97
|
+
const requestServicesLayer = Layer.mergeAll(SvelteKitCookies.layer, SvelteKitResponseHeaders.layer, SvelteKitLocals.layer);
|
|
98
|
+
/**
|
|
99
|
+
* Build all three request services directly from a {@link RequestEvent},
|
|
100
|
+
* without going through {@link SvelteKitRequestEvent}. Used internally by
|
|
101
|
+
* `requestLayer` in `app-runtime.ts` to avoid an extra indirection.
|
|
102
|
+
*/
|
|
103
|
+
const requestServicesFrom = (event) => {
|
|
104
|
+
const { cookies } = event;
|
|
105
|
+
return {
|
|
106
|
+
cookies: {
|
|
107
|
+
get: (...args) => Effect.sync(() => cookies.get(...args)),
|
|
108
|
+
getAll: (...args) => Effect.sync(() => cookies.getAll(...args)),
|
|
109
|
+
set: (...args) => Effect.sync(() => cookies.set(...args)),
|
|
110
|
+
delete: (...args) => Effect.sync(() => cookies.delete(...args)),
|
|
111
|
+
serialize: (...args) => Effect.sync(() => cookies.serialize(...args))
|
|
112
|
+
},
|
|
113
|
+
responseHeaders: { set: (headers) => Effect.sync(() => event.setHeaders(headers)) },
|
|
114
|
+
locals: event.locals
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
//#endregion
|
|
118
|
+
export { SvelteKitCookies, SvelteKitLocals, SvelteKitResponseHeaders, requestServicesFrom, requestServicesLayer };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=request-services.js.map
|