@thomasfosterau/effect-sveltekit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +163 -71
- package/dist/Navigation.d.ts +70 -17
- package/dist/Navigation.js +80 -19
- package/dist/Navigation.js.map +1 -1
- package/dist/Remote.d.ts +36 -18
- package/dist/Remote.js +20 -7
- package/dist/Remote.js.map +1 -1
- package/dist/State.d.ts +75 -6
- package/dist/State.js +116 -27
- package/dist/State.js.map +1 -1
- package/dist/forms.d.ts +70 -7
- package/dist/forms.js +66 -5
- package/dist/forms.js.map +1 -1
- package/dist/internal/app-state.d.ts +37 -2
- package/dist/internal/app-state.js +58 -15
- package/dist/internal/app-state.js.map +1 -1
- package/dist/internal/enhance.d.ts +106 -8
- package/dist/internal/enhance.js +30 -6
- package/dist/internal/enhance.js.map +1 -1
- package/dist/internal/watch.svelte.js +33 -0
- package/dist/internal/watch.svelte.js.map +1 -0
- package/dist/realtime.d.ts +28 -7
- package/dist/realtime.js +68 -43
- package/dist/realtime.js.map +1 -1
- package/package.json +6 -6
- package/LICENSE +0 -21
|
@@ -1 +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 \"$app/forms\";\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,CAAC,CAAC,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"}
|
|
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 \"$app/forms\";\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/**\n * Effect-based handlers for the two phases of an enhanced form submission,\n * plus `onError` for the errors they fail with. `ESubmit` / `EResult` are the\n * errors `onSubmit` / `onResult` may fail with, and `E` (their union by\n * default) the error `onError` receives. Each handler is called as a method of\n * the handlers object, so `this` in a handler written as a method is that\n * object.\n */\nexport interface EnhanceHandlers<\n R = never,\n ESubmit = never,\n EResult = never,\n E = ESubmit | EResult,\n> {\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 * If it fails with `ESubmit`, the submission is cancelled and the error goes\n * to {@link EnhanceHandlers.onError | onError}.\n */\n readonly onSubmit?: ((input: SubmitInput) => Effect.Effect<void, ESubmit, R>) | undefined;\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 * If it fails with `EResult`, the error goes to\n * {@link EnhanceHandlers.onError | onError}; SvelteKit's default behaviour\n * is not applied in its place.\n */\n readonly onResult?: ((options: ResultOptions) => Effect.Effect<void, EResult, R>) | undefined;\n /**\n * Handles an error `onSubmit` or `onResult` failed with (`E`, the union of\n * their error types), as `Effect.catch` would. It runs on the same runtime\n * and must not fail itself.\n *\n * Write it after `onSubmit` / `onResult` for `error` to be inferred\n * precisely. Written before a handler whose parameters are not annotated\n * (such as `onResult: ({ result }) => ...`), it still type-checks, but\n * `error` is `unknown`: TypeScript types `onError`'s parameter before it has\n * seen that handler's error type. An annotated parameter (or a function\n * passed by name) must accept every error the handlers fail with, in either\n * order.\n *\n * Only failures reach it: a defect or interruption (in a handler or in\n * `onError`) still rejects the promise SvelteKit awaits, as does a failure\n * when `onError` is omitted (rejecting with the error itself, as if the\n * handler ended in `Effect.orDie`). SvelteKit does not catch that rejection:\n * it sends no request after a rejected `onSubmit`, applies no default result\n * after a rejected `onResult`, and the browser reports an unhandled\n * rejection.\n */\n readonly onError?: ((error: E) => Effect.Effect<void, never, R>) | undefined;\n}\n\n/** Options for {@link makeSubmitFunction}. */\nexport interface EnhanceOptions<R = never> {\n /**\n * The runtime to run the handler effects against, providing every service\n * `R` they require. Required when the handlers (or `onError`) require\n * services; omit it when they require none (`R = never`), and they run on\n * the default runtime.\n */\n readonly runtime?: EnhanceRuntime<R> | undefined;\n}\n\n/**\n * The options argument of `Forms.enhance`, as a rest tuple: optional when the\n * handlers require no services (`R = never`), and otherwise required, with a\n * `runtime` that provides `R`. There is no ambient runtime to fall back on, so\n * handlers that require services and run without one would die with a\n * missing-service defect. A function that wraps `Forms.enhance` for a generic\n * `R` takes its options as `...options: EnhanceOptionsArgs<R>` and forwards\n * them unchanged.\n */\nexport type EnhanceOptionsArgs<R> = [R] extends [never]\n ? [options?: EnhanceOptions<never> | undefined]\n : [options: EnhanceOptions<R> & { readonly runtime: EnhanceRuntime<R> }];\n\n/** The handler names {@link makeSubmitFunction} accepts. */\nexport type EnhanceHandlerKey = keyof EnhanceHandlers;\n\n/**\n * Records which handlers a handlers object names: `Keys` is inferred from the\n * object's keys, which TypeScript knows before it types any handler.\n */\nexport type HandlerKeys<Keys extends EnhanceHandlerKey> = { readonly [Key in Keys]?: unknown };\n\n/**\n * The default of a handler's error (`ESubmit` / `EResult`) while TypeScript\n * has not inferred it: for a handler it types after `onError`, for one it\n * skips in its first inference pass (a handler whose parameters are not\n * annotated), or for one that is absent (and left out by `Keys`).\n *\n * It is `any` so that every handler type-checks against it, whatever it fails\n * with: when a call does not type-check, TypeScript may report the errors\n * against its first-pass inference, and a handler that did not fit this\n * default would then be blamed for a missing or wrong runtime. Detected with\n * {@link IsErrorNotInferred}, never left in `onError`'s parameter type.\n */\nexport type ErrorNotInferred = any;\n\n/** Whether `Errors` includes a handler error not inferred yet ({@link ErrorNotInferred}). */\nexport type IsErrorNotInferred<Errors> = 0 extends 1 & Errors ? true : false;\n\n/**\n * The default of a handler's requirements (`RSubmit` / `RResult` / `RError`)\n * while TypeScript has not inferred them: `unknown`, which every handler's\n * requirements are assignable to (see {@link ErrorNotInferred} for why), and\n * which {@link RequiredServices} counts as none.\n */\nexport type ServicesNotInferred = unknown;\n\n/**\n * The services a handler requires: `R`, or none while it is not inferred\n * ({@link ServicesNotInferred}), as for an absent handler.\n *\n * Since `unknown` must count as none, so does every requirements type that\n * `unknown` is assignable to: an effect typed as requiring `any` or `unknown`\n * (to which a union with `unknown` collapses), such as one annotated\n * `Effect.Effect<void, never, any>`, is treated as requiring no services, so\n * no runtime is demanded for it. Such a type names no service a runtime could\n * be checked against, and only a top type fits every handler in TypeScript's\n * first inference pass.\n */\nexport type RequiredServices<R> = unknown extends R ? never : R;\n\n/** The errors of the handlers named in `Keys`. */\nexport type HandlerErrors<Keys extends EnhanceHandlerKey, ESubmit, EResult> =\n | (\"onSubmit\" extends Keys ? ESubmit : never)\n | (\"onResult\" extends Keys ? EResult : never);\n\n/**\n * The error an unannotated `onError` receives: the handlers' errors, or\n * `unknown` while one of them is not inferred yet (`onError` written before a\n * handler whose parameters are not annotated).\n */\nexport type OnErrorInput<Errors> = IsErrorNotInferred<Errors> extends true ? unknown : Errors;\n\n/**\n * Requires `onError` to accept every error in `Errors` when its parameter is\n * annotated (`E` is then inferred from the annotation). The check waits while\n * a handler's error is not inferred yet, and applies once it is.\n */\nexport type OnErrorAccepts<R, Errors, E> =\n IsErrorNotInferred<Errors> extends true\n ? unknown\n : [Errors] extends [E]\n ? unknown\n : OnErrorMustAccept<R, Errors>;\n\n/** The `onError` that {@link OnErrorAccepts} requires. */\nexport interface OnErrorMustAccept<R, Errors> {\n readonly onError?: ((error: Errors) => Effect.Effect<void, never, R>) | undefined;\n}\n\n/** Run an `Effect<A, E, R>` on the given runtime (or the default when none). */\nconst runPromiseWith =\n <R>(runtime: EnhanceRuntime<R> | undefined) =>\n <A, E>(effect: Effect.Effect<A, E, R>): Promise<A> => {\n if (runtime === undefined) return Effect.runPromise(effect as Effect.Effect<A, E, never>);\n return Context.isContext(runtime)\n ? Effect.runPromiseWith(runtime)(effect)\n : runtime.runPromise(effect);\n };\n\n/**\n * The handlers {@link makeSubmitFunction} accepts: {@link EnhanceHandlers},\n * with each handler's requirements in a parameter of its own (`RSubmit`,\n * `RResult`, `RError`). TypeScript infers one candidate per handler and does\n * not union them, so a single requirements parameter would reject handlers\n * that require different services even when the runtime provides them all.\n */\nexport type EnhanceHandlersBy<RSubmit, RResult, RError, ESubmit, EResult, E> = Pick<\n EnhanceHandlers<RSubmit, ESubmit, EResult, E>,\n \"onSubmit\"\n> &\n Pick<EnhanceHandlers<RResult, ESubmit, EResult, E>, \"onResult\"> &\n Pick<EnhanceHandlers<RError, ESubmit, EResult, E>, \"onError\">;\n\n/**\n * Build a SvelteKit `SubmitFunction` from Effect-based handlers. See\n * `Forms.enhance` for the public entry point and documentation.\n *\n * The type parameters are inferred, never written. `RSubmit`, `RResult` and\n * `RError` are the services each handler requires (see\n * {@link EnhanceHandlersBy}). `Keys` names the handlers present, and `E` (the\n * error `onError` receives) is computed from their errors, or inferred from\n * `onError`'s parameter annotation and then checked against them, so a\n * handlers object type-checks whatever its property order (see\n * {@link EnhanceHandlers.onError}). The options argument, and its `runtime`,\n * are required when the handlers require any service, and the runtime must\n * provide all of them (see {@link EnhanceOptionsArgs}).\n *\n * When a call does not type-check, TypeScript may report it against its first\n * inference pass, which skips handlers whose parameters are not annotated. The\n * defaults of the handler parameters ({@link ErrorNotInferred},\n * {@link ServicesNotInferred}) accept any handler, so a missing or wrong\n * runtime is reported on the call or on the runtime, never on a handler.\n */\nexport const makeSubmitFunction = <\n RSubmit = ServicesNotInferred,\n RResult = ServicesNotInferred,\n RError = ServicesNotInferred,\n ESubmit = ErrorNotInferred,\n EResult = ErrorNotInferred,\n Keys extends EnhanceHandlerKey = never,\n E = OnErrorInput<HandlerErrors<Keys, ESubmit, EResult>>,\n>(\n handlers: EnhanceHandlersBy<RSubmit, RResult, RError, ESubmit, EResult, E> &\n HandlerKeys<Keys> &\n NoInfer<OnErrorAccepts<RError, HandlerErrors<Keys, ESubmit, EResult>, E>>,\n // The requirements are inferred from the handlers alone; the runtime is\n // then checked against their union.\n ...rest: NoInfer<\n EnhanceOptionsArgs<\n RequiredServices<RSubmit> | RequiredServices<RResult> | RequiredServices<RError>\n >\n >\n): SubmitFunction => {\n type R = RequiredServices<RSubmit> | RequiredServices<RResult> | RequiredServices<RError>;\n // Either branch of `EnhanceOptionsArgs<R>`: the `never` one only when `R`\n // is `never`, where the two coincide.\n const options = rest[0] as EnhanceOptions<R> | undefined;\n const run = runPromiseWith(options?.runtime);\n // Each handler requires a part of `R`, which the runtime provides in full.\n const phases = handlers as EnhanceHandlers<R, ESubmit, EResult, E>;\n // `E` is the union of the errors of the handlers present, so `onError`\n // accepts whatever either handler fails with.\n const onError = handlers.onError as\n | ((error: ESubmit | EResult) => Effect.Effect<void, never, R>)\n | undefined;\n // Each handler is called as a method of the handlers object (`this` is\n // `handlers`), as 0.5.0 called `onSubmit`.\n const handleError =\n onError === undefined ? undefined : (error: ESubmit | EResult) => onError.call(handlers, error);\n return async (input) => {\n // `onSubmit` and `onResult` are read on each submission, so a handler\n // replaced on the handlers object after `enhance()` is the one that runs.\n // `onResult` is read once `onSubmit` has run, so `onSubmit` may set it.\n const submit = phases.onSubmit?.(input);\n if (submit !== undefined) {\n await run(\n handleError === undefined\n ? submit\n : submit.pipe(\n Effect.catch((error) =>\n Effect.sync(() => input.cancel()).pipe(Effect.andThen(handleError(error))),\n ),\n ),\n );\n }\n const { onResult } = phases;\n // Returning `undefined` lets SvelteKit apply its default result behaviour.\n if (!onResult) return undefined;\n return (resultOptions) => {\n const result = onResult.call(handlers, resultOptions);\n return run(handleError === undefined ? result : result.pipe(Effect.catch(handleError)));\n };\n };\n};\n"],"mappings":";;;AAoLA,MAAM,kBACA,aACG,WAA+C;CACpD,IAAI,YAAY,KAAA,GAAW,OAAO,OAAO,WAAW,MAAoC;CACxF,OAAO,QAAQ,UAAU,OAAO,IAC5B,OAAO,eAAe,OAAO,CAAC,CAAC,MAAM,IACrC,QAAQ,WAAW,MAAM;AAC/B;;;;;;;;;;;;;;;;;;;;;AAoCF,MAAa,sBASX,UAKA,GAAG,SAKgB;CAInB,MAAM,UAAU,KAAK;CACrB,MAAM,MAAM,eAAe,SAAS,OAAO;CAE3C,MAAM,SAAS;CAGf,MAAM,UAAU,SAAS;CAKzB,MAAM,cACJ,YAAY,KAAA,IAAY,KAAA,KAAa,UAA6B,QAAQ,KAAK,UAAU,KAAK;CAChG,OAAO,OAAO,UAAU;EAItB,MAAM,SAAS,OAAO,WAAW,KAAK;EACtC,IAAI,WAAW,KAAA,GACb,MAAM,IACJ,gBAAgB,KAAA,IACZ,SACA,OAAO,KACL,OAAO,OAAO,UACZ,OAAO,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,YAAY,KAAK,CAAC,CAAC,CAC3E,CACF,CACN;EAEF,MAAM,EAAE,aAAa;EAErB,IAAI,CAAC,UAAU,OAAO,KAAA;EACtB,QAAQ,kBAAkB;GACxB,MAAM,SAAS,SAAS,KAAK,UAAU,aAAa;GACpD,OAAO,IAAI,gBAAgB,KAAA,IAAY,SAAS,OAAO,KAAK,OAAO,MAAM,WAAW,CAAC,CAAC;EACxF;CACF;AACF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
//#region src/internal/watch.svelte.ts
|
|
3
|
+
/**
|
|
4
|
+
* The one runes-dependent piece of the `$app/state` bridge: watch a reactive
|
|
5
|
+
* read outside any component.
|
|
6
|
+
*
|
|
7
|
+
* `$app/state`'s `page` / `navigating` / `updated` are rune-backed, and Svelte
|
|
8
|
+
* offers no rune-free way to observe a signal, so this module is a `.svelte.ts`
|
|
9
|
+
* file (kept as `.svelte.js` in `dist/`, where the consumer's Svelte compiler
|
|
10
|
+
* processes it). Everything else lives in the rune-free `app-state.ts`,
|
|
11
|
+
* parameterised over {@link watch}, so it can be unit-tested in plain Node.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Calls `onValue` with `read()` on the next Svelte flush, then again each time
|
|
17
|
+
* a reactive value `read` touched changes. The effect lives in its own
|
|
18
|
+
* `$effect.root`, so it needs no component context; the returned function
|
|
19
|
+
* destroys it. On the server Svelte never runs effects, so `onValue` is never
|
|
20
|
+
* called there.
|
|
21
|
+
*/
|
|
22
|
+
function watch(read, onValue) {
|
|
23
|
+
return $effect.root(() => {
|
|
24
|
+
$effect(() => {
|
|
25
|
+
const value = read();
|
|
26
|
+
untrack(() => onValue(value));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { watch };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=watch.svelte.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.svelte.js","names":[],"sources":["../../src/internal/watch.svelte.ts"],"sourcesContent":["/**\n * The one runes-dependent piece of the `$app/state` bridge: watch a reactive\n * read outside any component.\n *\n * `$app/state`'s `page` / `navigating` / `updated` are rune-backed, and Svelte\n * offers no rune-free way to observe a signal, so this module is a `.svelte.ts`\n * file (kept as `.svelte.js` in `dist/`, where the consumer's Svelte compiler\n * processes it). Everything else lives in the rune-free `app-state.ts`,\n * parameterised over {@link watch}, so it can be unit-tested in plain Node.\n *\n * @internal\n */\n\nimport { untrack } from \"svelte\";\n\n/**\n * Calls `onValue` with `read()` on the next Svelte flush, then again each time\n * a reactive value `read` touched changes. The effect lives in its own\n * `$effect.root`, so it needs no component context; the returned function\n * destroys it. On the server Svelte never runs effects, so `onValue` is never\n * called there.\n */\nexport function watch<A>(read: () => A, onValue: (value: A) => void): () => void {\n return $effect.root(() => {\n $effect(() => {\n const value = read();\n // Only `read` should be tracked; delivering the value must not subscribe\n // the effect to anything the consumer happens to touch.\n untrack(() => onValue(value));\n });\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,MAAS,MAAe,SAAyC;CAC/E,OAAO,QAAQ,WAAW;EACxB,cAAc;GACZ,MAAM,QAAQ,KAAK;GAGnB,cAAc,QAAQ,KAAK,CAAC;EAC9B,CAAC;CACH,CAAC;AACH"}
|
package/dist/realtime.d.ts
CHANGED
|
@@ -14,9 +14,16 @@ interface SseMessage {
|
|
|
14
14
|
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 & {
|
|
15
15
|
readonly _tag: "EventSourceError";
|
|
16
16
|
} & Readonly<A>;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Raised when an `EventSource` cannot be opened (its constructor throws, e.g.
|
|
19
|
+
* for an invalid URL; `cause` holds the thrown value) or its connection fails
|
|
20
|
+
* fatally (its `readyState` is `CLOSED`; `cause` is absent).
|
|
21
|
+
*/
|
|
18
22
|
declare class EventSourceError extends EventSourceError_base<{
|
|
23
|
+
/** The URL the `EventSource` was opened with. */
|
|
19
24
|
readonly url: string;
|
|
25
|
+
/** What the `EventSource` constructor threw, when that is why the stream failed. */
|
|
26
|
+
readonly cause?: unknown;
|
|
20
27
|
}> {}
|
|
21
28
|
/** The subset of the `EventSource` constructor this module relies on. */
|
|
22
29
|
type EventSourceConstructor = new (url: string, eventSourceInitDict?: {
|
|
@@ -43,8 +50,11 @@ interface FromEventSourceOptions {
|
|
|
43
50
|
* Consume a Server-Sent-Events endpoint as a `Stream` of {@link SseMessage}.
|
|
44
51
|
*
|
|
45
52
|
* The browser's `EventSource` reconnects automatically after transient drops,
|
|
46
|
-
* so the stream stays live across reconnects
|
|
47
|
-
* {@link EventSourceError}
|
|
53
|
+
* so the stream stays live across reconnects. It fails with
|
|
54
|
+
* {@link EventSourceError} when the connection closes fatally, or, with the
|
|
55
|
+
* thrown value as its `cause`, when the `EventSource` constructor throws (e.g.
|
|
56
|
+
* a `SyntaxError` for an invalid URL). The source is closed when the stream's
|
|
57
|
+
* scope closes; one that failed to construct has nothing to close.
|
|
48
58
|
*
|
|
49
59
|
* @example
|
|
50
60
|
* ```ts
|
|
@@ -60,11 +70,20 @@ declare const fromEventSource: (url: string, options?: FromEventSourceOptions) =
|
|
|
60
70
|
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 & {
|
|
61
71
|
readonly _tag: "WebSocketError";
|
|
62
72
|
} & Readonly<A>;
|
|
63
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Raised when a `WebSocket` cannot be opened (its constructor throws, e.g. for
|
|
75
|
+
* an invalid URL or protocol list; `cause` holds the thrown value), errors, or
|
|
76
|
+
* closes uncleanly (`code` and `reason` come from the close event).
|
|
77
|
+
*/
|
|
64
78
|
declare class WebSocketError extends WebSocketError_base<{
|
|
79
|
+
/** The URL the `WebSocket` was opened with. */
|
|
65
80
|
readonly url: string;
|
|
81
|
+
/** The close code, when an unclean close is why the stream failed. */
|
|
66
82
|
readonly code?: number;
|
|
83
|
+
/** The close reason, when an unclean close is why the stream failed. */
|
|
67
84
|
readonly reason?: string;
|
|
85
|
+
/** What the `WebSocket` constructor threw, when that is why the stream failed. */
|
|
86
|
+
readonly cause?: unknown;
|
|
68
87
|
}> {}
|
|
69
88
|
/** The subset of the `WebSocket` constructor this module relies on. */
|
|
70
89
|
type WebSocketConstructor = new (url: string, protocols?: string | ReadonlyArray<string>) => WebSocket;
|
|
@@ -85,9 +104,11 @@ interface FromWebSocketOptions {
|
|
|
85
104
|
* Consume a `WebSocket` as a `Stream` of its message payloads
|
|
86
105
|
* (`string | ArrayBuffer | Blob`, depending on `binaryType`).
|
|
87
106
|
*
|
|
88
|
-
* The stream ends when the socket closes cleanly
|
|
89
|
-
* {@link WebSocketError} on an error or unclean close
|
|
90
|
-
* the
|
|
107
|
+
* The stream ends when the socket closes cleanly. It fails with
|
|
108
|
+
* {@link WebSocketError} on an error or unclean close, or, with the thrown
|
|
109
|
+
* value as its `cause`, when the `WebSocket` constructor throws (e.g. a
|
|
110
|
+
* `SyntaxError` for an invalid URL or protocol list). The socket is closed when
|
|
111
|
+
* the stream's scope closes; one that failed to construct has nothing to close.
|
|
91
112
|
*
|
|
92
113
|
* @example
|
|
93
114
|
* ```ts
|
package/dist/realtime.js
CHANGED
|
@@ -17,14 +17,21 @@ var realtime_exports = /* @__PURE__ */ __exportAll({
|
|
|
17
17
|
fromEventSource: () => fromEventSource,
|
|
18
18
|
fromWebSocket: () => fromWebSocket
|
|
19
19
|
});
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Raised when an `EventSource` cannot be opened (its constructor throws, e.g.
|
|
22
|
+
* for an invalid URL; `cause` holds the thrown value) or its connection fails
|
|
23
|
+
* fatally (its `readyState` is `CLOSED`; `cause` is absent).
|
|
24
|
+
*/
|
|
21
25
|
var EventSourceError = class extends Data.TaggedError("EventSourceError") {};
|
|
22
26
|
/**
|
|
23
27
|
* Consume a Server-Sent-Events endpoint as a `Stream` of {@link SseMessage}.
|
|
24
28
|
*
|
|
25
29
|
* The browser's `EventSource` reconnects automatically after transient drops,
|
|
26
|
-
* so the stream stays live across reconnects
|
|
27
|
-
* {@link EventSourceError}
|
|
30
|
+
* so the stream stays live across reconnects. It fails with
|
|
31
|
+
* {@link EventSourceError} when the connection closes fatally, or, with the
|
|
32
|
+
* thrown value as its `cause`, when the `EventSource` constructor throws (e.g.
|
|
33
|
+
* a `SyntaxError` for an invalid URL). The source is closed when the stream's
|
|
34
|
+
* scope closes; one that failed to construct has nothing to close.
|
|
28
35
|
*
|
|
29
36
|
* @example
|
|
30
37
|
* ```ts
|
|
@@ -38,35 +45,47 @@ var EventSourceError = class extends Data.TaggedError("EventSourceError") {};
|
|
|
38
45
|
*/
|
|
39
46
|
const fromEventSource = (url, options) => {
|
|
40
47
|
const Impl = options?.implementation ?? globalThis.EventSource;
|
|
41
|
-
return Stream.callback((queue) => Effect.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
return Stream.callback((queue) => Effect.acquireRelease(Effect.try({
|
|
49
|
+
try: () => {
|
|
50
|
+
const source = options?.withCredentials === void 0 ? new Impl(url) : new Impl(url, { withCredentials: options.withCredentials });
|
|
51
|
+
const onMessage = (e) => {
|
|
52
|
+
Queue.offerUnsafe(queue, {
|
|
53
|
+
event: e.type,
|
|
54
|
+
data: e.data,
|
|
55
|
+
lastEventId: e.lastEventId
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
source.addEventListener("message", onMessage);
|
|
59
|
+
for (const name of options?.events ?? []) source.addEventListener(name, onMessage);
|
|
60
|
+
source.addEventListener("error", () => {
|
|
61
|
+
if (source.readyState === 2) Queue.failCauseUnsafe(queue, Cause.fail(new EventSourceError({ url })));
|
|
48
62
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
}), {
|
|
63
|
+
return source;
|
|
64
|
+
},
|
|
65
|
+
catch: (cause) => new EventSourceError({
|
|
66
|
+
url,
|
|
67
|
+
cause
|
|
68
|
+
})
|
|
69
|
+
}), (source) => Effect.sync(() => source.close())), {
|
|
57
70
|
bufferSize: options?.bufferSize ?? 16,
|
|
58
71
|
strategy: options?.strategy ?? "sliding"
|
|
59
72
|
});
|
|
60
73
|
};
|
|
61
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Raised when a `WebSocket` cannot be opened (its constructor throws, e.g. for
|
|
76
|
+
* an invalid URL or protocol list; `cause` holds the thrown value), errors, or
|
|
77
|
+
* closes uncleanly (`code` and `reason` come from the close event).
|
|
78
|
+
*/
|
|
62
79
|
var WebSocketError = class extends Data.TaggedError("WebSocketError") {};
|
|
63
80
|
/**
|
|
64
81
|
* Consume a `WebSocket` as a `Stream` of its message payloads
|
|
65
82
|
* (`string | ArrayBuffer | Blob`, depending on `binaryType`).
|
|
66
83
|
*
|
|
67
|
-
* The stream ends when the socket closes cleanly
|
|
68
|
-
* {@link WebSocketError} on an error or unclean close
|
|
69
|
-
* the
|
|
84
|
+
* The stream ends when the socket closes cleanly. It fails with
|
|
85
|
+
* {@link WebSocketError} on an error or unclean close, or, with the thrown
|
|
86
|
+
* value as its `cause`, when the `WebSocket` constructor throws (e.g. a
|
|
87
|
+
* `SyntaxError` for an invalid URL or protocol list). The socket is closed when
|
|
88
|
+
* the stream's scope closes; one that failed to construct has nothing to close.
|
|
70
89
|
*
|
|
71
90
|
* @example
|
|
72
91
|
* ```ts
|
|
@@ -80,27 +99,33 @@ var WebSocketError = class extends Data.TaggedError("WebSocketError") {};
|
|
|
80
99
|
*/
|
|
81
100
|
const fromWebSocket = (url, options) => {
|
|
82
101
|
const Impl = options?.implementation ?? globalThis.WebSocket;
|
|
83
|
-
return Stream.callback((queue) => Effect.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
102
|
+
return Stream.callback((queue) => Effect.acquireRelease(Effect.try({
|
|
103
|
+
try: () => {
|
|
104
|
+
const socket = new Impl(url, options?.protocols);
|
|
105
|
+
if (options?.binaryType !== void 0) socket.binaryType = options.binaryType;
|
|
106
|
+
socket.addEventListener("message", (e) => {
|
|
107
|
+
Queue.offerUnsafe(queue, e.data);
|
|
108
|
+
});
|
|
109
|
+
socket.addEventListener("error", () => {
|
|
110
|
+
Queue.failCauseUnsafe(queue, Cause.fail(new WebSocketError({ url })));
|
|
111
|
+
});
|
|
112
|
+
socket.addEventListener("close", (e) => {
|
|
113
|
+
if (e.wasClean) Queue.endUnsafe(queue);
|
|
114
|
+
else Queue.failCauseUnsafe(queue, Cause.fail(new WebSocketError({
|
|
115
|
+
url,
|
|
116
|
+
code: e.code,
|
|
117
|
+
reason: e.reason
|
|
118
|
+
})));
|
|
119
|
+
});
|
|
120
|
+
return socket;
|
|
121
|
+
},
|
|
122
|
+
catch: (cause) => new WebSocketError({
|
|
123
|
+
url,
|
|
124
|
+
cause
|
|
125
|
+
})
|
|
126
|
+
}), (socket) => Effect.sync(() => {
|
|
127
|
+
if (socket.readyState === 0 || socket.readyState === 1) socket.close();
|
|
128
|
+
})), {
|
|
104
129
|
bufferSize: options?.bufferSize ?? 16,
|
|
105
130
|
strategy: options?.strategy ?? "suspend"
|
|
106
131
|
});
|
package/dist/realtime.js.map
CHANGED
|
@@ -1 +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\n/** Options for {@link fromEventSource}. */\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\n/** Options for {@link fromWebSocket}. */\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,CAAC,CAEvE,CAAC;;;;;;;;;;;;;;;;;;AA2CJ,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,CAAC,CAInE,CAAC;;;;;;;;;;;;;;;;;;;AAwCJ,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"}
|
|
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/**\n * Raised when an `EventSource` cannot be opened (its constructor throws, e.g.\n * for an invalid URL; `cause` holds the thrown value) or its connection fails\n * fatally (its `readyState` is `CLOSED`; `cause` is absent).\n */\nexport class EventSourceError extends Data.TaggedError(\"EventSourceError\")<{\n /** The URL the `EventSource` was opened with. */\n readonly url: string;\n /** What the `EventSource` constructor threw, when that is why the stream failed. */\n readonly cause?: unknown;\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\n/** Options for {@link fromEventSource}. */\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} when the connection closes fatally, or, with the\n * thrown value as its `cause`, when the `EventSource` constructor throws (e.g.\n * a `SyntaxError` for an invalid URL). The source is closed when the stream's\n * scope closes; one that failed to construct has nothing to close.\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 // Effect v4's `Stream.callback` runs the returned Effect as the stream's\n // setup, inside the stream's scope; it is not a cleanup. Open the source\n // with `acquireRelease` so it is closed when that scope closes.\n return Stream.callback<SseMessage, EventSourceError>(\n (queue) =>\n Effect.acquireRelease(\n // Construct and attach the listeners in one synchronous step, so no\n // event can be dispatched in between; a throw from either (in practice\n // the constructor) becomes the typed error. A failed acquire registers\n // no release: a source that failed to construct has nothing to close.\n Effect.try({\n try: () => {\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 source;\n },\n catch: (cause) => new EventSourceError({ url, cause }),\n }),\n (source) => Effect.sync(() => source.close()),\n ),\n { bufferSize: options?.bufferSize ?? 16, strategy: options?.strategy ?? \"sliding\" },\n );\n};\n\n// ---------------------------------------------------------------------------\n// WebSocket\n// ---------------------------------------------------------------------------\n\n/**\n * Raised when a `WebSocket` cannot be opened (its constructor throws, e.g. for\n * an invalid URL or protocol list; `cause` holds the thrown value), errors, or\n * closes uncleanly (`code` and `reason` come from the close event).\n */\nexport class WebSocketError extends Data.TaggedError(\"WebSocketError\")<{\n /** The URL the `WebSocket` was opened with. */\n readonly url: string;\n /** The close code, when an unclean close is why the stream failed. */\n readonly code?: number;\n /** The close reason, when an unclean close is why the stream failed. */\n readonly reason?: string;\n /** What the `WebSocket` constructor threw, when that is why the stream failed. */\n readonly cause?: unknown;\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\n/** Options for {@link fromWebSocket}. */\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. It fails with\n * {@link WebSocketError} on an error or unclean close, or, with the thrown\n * value as its `cause`, when the `WebSocket` constructor throws (e.g. a\n * `SyntaxError` for an invalid URL or protocol list). The socket is closed when\n * the stream's scope closes; one that failed to construct has nothing to close.\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.acquireRelease(\n // See `fromEventSource`: construct and attach the listeners in one\n // synchronous step, mapping a throw to the typed error; a failed\n // acquire registers no release.\n Effect.try({\n try: () => {\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 socket;\n },\n catch: (cause) => new WebSocketError({ url, cause }),\n }),\n (socket) =>\n 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":";;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,mBAAb,cAAsC,KAAK,YAAY,kBAAkB,CAAC,CAKvE,CAAC;;;;;;;;;;;;;;;;;;;;;AA8CJ,MAAa,mBACX,KACA,YACgD;CAChD,MAAM,OAAO,SAAS,kBAAmB,WAAW;CAIpD,OAAO,OAAO,UACX,UACC,OAAO,eAKL,OAAO,IAAI;EACT,WAAW;GACT,MAAM,SACJ,SAAS,oBAAoB,KAAA,IACzB,IAAI,KAAK,GAAG,IACZ,IAAI,KAAK,KAAK,EAAE,iBAAiB,QAAQ,gBAAgB,CAAC;GAEhE,MAAM,aAAa,MAA0B;IAC3C,MAAM,YAAY,OAAO;KAAE,OAAO,EAAE;KAAM,MAAM,EAAE;KAAM,aAAa,EAAE;IAAY,CAAC;GACtF;GAEA,OAAO,iBAAiB,WAAW,SAAS;GAC5C,KAAK,MAAM,QAAQ,SAAS,UAAU,CAAC,GAAG,OAAO,iBAAiB,MAAM,SAAS;GAEjF,OAAO,iBAAiB,eAAe;IAErC,IAAI,OAAO,eAAe,GACxB,MAAM,gBAAgB,OAAO,MAAM,KAAK,IAAI,iBAAiB,EAAE,IAAI,CAAC,CAAC,CAAC;GAE1E,CAAC;GAED,OAAO;EACT;EACA,QAAQ,UAAU,IAAI,iBAAiB;GAAE;GAAK;EAAM,CAAC;CACvD,CAAC,IACA,WAAW,OAAO,WAAW,OAAO,MAAM,CAAC,CAC9C,GACF;EAAE,YAAY,SAAS,cAAc;EAAI,UAAU,SAAS,YAAY;CAAU,CACpF;AACF;;;;;;AAWA,IAAa,iBAAb,cAAoC,KAAK,YAAY,gBAAgB,CAAC,CASnE,CAAC;;;;;;;;;;;;;;;;;;;;;AA0CJ,MAAa,iBACX,KACA,YAC+D;CAC/D,MAAM,OAAO,SAAS,kBAAmB,WAAW;CACpD,OAAO,OAAO,UACX,UACC,OAAO,eAIL,OAAO,IAAI;EACT,WAAW;GACT,MAAM,SAAS,IAAI,KAAK,KAAK,SAAS,SAAS;GAC/C,IAAI,SAAS,eAAe,KAAA,GAAW,OAAO,aAAa,QAAQ;GAEnE,OAAO,iBAAiB,YAAY,MAAoB;IACtD,MAAM,YAAY,OAAO,EAAE,IAAI;GACjC,CAAC;GAED,OAAO,iBAAiB,eAAe;IACrC,MAAM,gBAAgB,OAAO,MAAM,KAAK,IAAI,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC;GACtE,CAAC;GAED,OAAO,iBAAiB,UAAU,MAAkB;IAClD,IAAI,EAAE,UACJ,MAAM,UAAU,KAAK;SAErB,MAAM,gBACJ,OACA,MAAM,KAAK,IAAI,eAAe;KAAE;KAAK,MAAM,EAAE;KAAM,QAAQ,EAAE;IAAO,CAAC,CAAC,CACxE;GAEJ,CAAC;GAED,OAAO;EACT;EACA,QAAQ,UAAU,IAAI,eAAe;GAAE;GAAK;EAAM,CAAC;CACrD,CAAC,IACA,WACC,OAAO,WAAW;EAEhB,IAAI,OAAO,eAAe,KAAK,OAAO,eAAe,GAAG,OAAO,MAAM;CACvE,CAAC,CACL,GACF;EAAE,YAAY,SAAS,cAAc;EAAI,UAAU,SAAS,YAAY;CAAU,CACpF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomasfosterau/effect-sveltekit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Effect integration for SvelteKit: load/action/endpoint wrappers, remote functions, an effectKit() Vite plugin, and an Effect HTTP deployment adapter",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -73,19 +73,19 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"@standard-schema/spec": "^1.1.0",
|
|
76
|
-
"magic-string": "^1.4.
|
|
77
|
-
"@thomasfosterau/effect-svelte": "^0.
|
|
76
|
+
"magic-string": "^1.4.3",
|
|
77
|
+
"@thomasfosterau/effect-svelte": "^0.5.0"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@effect/platform-node": "4.0.
|
|
80
|
+
"@effect/platform-node": "4.0.2",
|
|
81
81
|
"@sveltejs/kit": "^3.0.0",
|
|
82
82
|
"svelte": "^5.57.1",
|
|
83
83
|
"typescript": "npm:@typescript/typescript6@6.0.2"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
|
-
"@effect/platform-node": ">=4.0.
|
|
86
|
+
"@effect/platform-node": ">=4.0.2",
|
|
87
87
|
"@sveltejs/kit": "^3.0.0",
|
|
88
|
-
"effect": ">=4.0.
|
|
88
|
+
"effect": ">=4.0.2",
|
|
89
89
|
"svelte": "^5.40.0",
|
|
90
90
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
91
91
|
},
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Thomas Foster
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|