@thomasfosterau/effect-svelte 0.1.0 → 0.3.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.
Files changed (46) hide show
  1. package/dist/await.svelte.d.ts +53 -0
  2. package/dist/await.svelte.js +44 -0
  3. package/dist/await.svelte.js.map +1 -0
  4. package/dist/context.svelte.d.ts +51 -1
  5. package/dist/context.svelte.js +43 -1
  6. package/dist/context.svelte.js.map +1 -1
  7. package/dist/derived.svelte.d.ts +32 -12
  8. package/dist/derived.svelte.js +23 -13
  9. package/dist/derived.svelte.js.map +1 -1
  10. package/dist/effect.svelte.d.ts +21 -2
  11. package/dist/effect.svelte.js +13 -3
  12. package/dist/effect.svelte.js.map +1 -1
  13. package/dist/index.d.ts +12 -7
  14. package/dist/index.js +6 -2
  15. package/dist/internal/await.js +52 -0
  16. package/dist/internal/await.js.map +1 -0
  17. package/dist/internal/live-stream.js +43 -0
  18. package/dist/internal/live-stream.js.map +1 -0
  19. package/dist/internal/mutation.d.ts +38 -0
  20. package/dist/internal/mutation.js +61 -0
  21. package/dist/internal/mutation.js.map +1 -0
  22. package/dist/internal/result.svelte.d.ts +1 -0
  23. package/dist/internal/writable.js +21 -0
  24. package/dist/internal/writable.js.map +1 -0
  25. package/dist/live-stream.svelte.d.ts +72 -0
  26. package/dist/live-stream.svelte.js +77 -0
  27. package/dist/live-stream.svelte.js.map +1 -0
  28. package/dist/mutation.svelte.d.ts +65 -0
  29. package/dist/mutation.svelte.js +53 -0
  30. package/dist/mutation.svelte.js.map +1 -0
  31. package/dist/query.svelte.d.ts +21 -2
  32. package/dist/query.svelte.js +10 -3
  33. package/dist/query.svelte.js.map +1 -1
  34. package/dist/reactivity.svelte.d.ts +15 -5
  35. package/dist/reactivity.svelte.js +8 -8
  36. package/dist/reactivity.svelte.js.map +1 -1
  37. package/dist/stream.svelte.d.ts +10 -2
  38. package/dist/stream.svelte.js +2 -2
  39. package/dist/stream.svelte.js.map +1 -1
  40. package/dist/subscription.svelte.d.ts +12 -3
  41. package/dist/subscription.svelte.js +4 -4
  42. package/dist/subscription.svelte.js.map +1 -1
  43. package/dist/writable-ref.svelte.d.ts +53 -0
  44. package/dist/writable-ref.svelte.js +66 -0
  45. package/dist/writable-ref.svelte.js.map +1 -0
  46. package/package.json +1 -1
@@ -0,0 +1,53 @@
1
+ import { RuntimeLike } from "./internal/run.js";
2
+ import { Effect } from "effect";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+
5
+ //#region src/await.svelte.d.ts
6
+ interface UseQueryPromiseOptions<R> {
7
+ /**
8
+ * Run against this runtime instead of the ambient context. When provided, the
9
+ * effect's `R` is constrained to what the runtime provides.
10
+ */
11
+ readonly runtime?: RuntimeLike<R>;
12
+ }
13
+ interface UseQueryPromiseReturn<A, E> {
14
+ /** The underlying query result state (same as {@link useQuery}). */
15
+ readonly current: AsyncResult.AsyncResult<A, E>;
16
+ /**
17
+ * A `Promise` reflecting `current`, for use with Svelte's `{#await}` (or
18
+ * `<svelte:boundary>`). Its identity is stable until the result transitions:
19
+ * pending while loading, resolved with the (possibly stale) value, rejected
20
+ * on failure.
21
+ */
22
+ readonly promise: Promise<A>;
23
+ /** Re-run the effect. A new `promise` is produced as the result transitions. */
24
+ readonly refetch: () => void;
25
+ }
26
+ /**
27
+ * Run an effect and bridge its result to Svelte 5's native async, so you can
28
+ * render it with an `{#await}` block instead of `AsyncResult.match`.
29
+ *
30
+ * @example
31
+ * ```svelte
32
+ * <script lang="ts">
33
+ * import { useQueryPromise } from '@thomasfosterau/effect-svelte';
34
+ * import { Effect } from 'effect';
35
+ *
36
+ * const user = useQueryPromise(fetchUser(1));
37
+ * </script>
38
+ *
39
+ * {#await user.promise}
40
+ * <p>Loading…</p>
41
+ * {:then value}
42
+ * <p>{value.name}</p>
43
+ * {:catch error}
44
+ * <p>Failed: {error.message}</p>
45
+ * {/await}
46
+ *
47
+ * <button onclick={user.refetch}>Reload</button>
48
+ * ```
49
+ */
50
+ declare function useQueryPromise<A, E, R>(effect: Effect.Effect<A, E, R>, options?: UseQueryPromiseOptions<R>): UseQueryPromiseReturn<A, E>;
51
+ //#endregion
52
+ export { UseQueryPromiseOptions, UseQueryPromiseReturn, useQueryPromise };
53
+ //# sourceMappingURL=await.svelte.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { resultToPromise } from "./internal/await.js";
2
+ import { useQuery } from "./query.svelte.js";
3
+ //#region src/await.svelte.ts
4
+ /**
5
+ * Run an effect and bridge its result to Svelte 5's native async, so you can
6
+ * render it with an `{#await}` block instead of `AsyncResult.match`.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <script lang="ts">
11
+ * import { useQueryPromise } from '@thomasfosterau/effect-svelte';
12
+ * import { Effect } from 'effect';
13
+ *
14
+ * const user = useQueryPromise(fetchUser(1));
15
+ * <\/script>
16
+ *
17
+ * {#await user.promise}
18
+ * <p>Loading…</p>
19
+ * {:then value}
20
+ * <p>{value.name}</p>
21
+ * {:catch error}
22
+ * <p>Failed: {error.message}</p>
23
+ * {/await}
24
+ *
25
+ * <button onclick={user.refetch}>Reload</button>
26
+ * ```
27
+ */
28
+ function useQueryPromise(effect, options = {}) {
29
+ const query = useQuery(effect, options);
30
+ const promise = $derived(resultToPromise(query.current));
31
+ return {
32
+ get current() {
33
+ return query.current;
34
+ },
35
+ get promise() {
36
+ return promise;
37
+ },
38
+ refetch: query.refetch
39
+ };
40
+ }
41
+ //#endregion
42
+ export { useQueryPromise };
43
+
44
+ //# sourceMappingURL=await.svelte.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"await.svelte.js","names":[],"sources":["../src/await.svelte.ts"],"sourcesContent":["import type { Effect } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { resultToPromise } from \"./internal/await.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\nimport { useQuery } from \"./query.svelte.js\";\n\nexport interface UseQueryPromiseOptions<R> {\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * effect's `R` is constrained to what the runtime provides.\n */\n readonly runtime?: RuntimeLike<R>;\n}\n\nexport interface UseQueryPromiseReturn<A, E> {\n /** The underlying query result state (same as {@link useQuery}). */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * A `Promise` reflecting `current`, for use with Svelte's `{#await}` (or\n * `<svelte:boundary>`). Its identity is stable until the result transitions:\n * pending while loading, resolved with the (possibly stale) value, rejected\n * on failure.\n */\n readonly promise: Promise<A>;\n\n /** Re-run the effect. A new `promise` is produced as the result transitions. */\n readonly refetch: () => void;\n}\n\n/**\n * Run an effect and bridge its result to Svelte 5's native async, so you can\n * render it with an `{#await}` block instead of `AsyncResult.match`.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useQueryPromise } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const user = useQueryPromise(fetchUser(1));\n * </script>\n *\n * {#await user.promise}\n * <p>Loading…</p>\n * {:then value}\n * <p>{value.name}</p>\n * {:catch error}\n * <p>Failed: {error.message}</p>\n * {/await}\n *\n * <button onclick={user.refetch}>Reload</button>\n * ```\n */\nexport function useQueryPromise<A, E, R>(\n effect: Effect.Effect<A, E, R>,\n options: UseQueryPromiseOptions<R> = {},\n): UseQueryPromiseReturn<A, E> {\n const query = useQuery(effect, options);\n\n // Recompute (and only then hand `{#await}` a new promise) when the underlying\n // result transitions — not on every unrelated re-render.\n const promise = $derived(resultToPromise(query.current));\n\n return {\n get current() {\n return query.current;\n },\n get promise() {\n return promise;\n },\n refetch: query.refetch,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAgB,gBACd,QACA,UAAqC,CAAC,GACT;CAC7B,MAAM,QAAQ,SAAS,QAAQ,OAAO;CAItC,MAAM,UAAU,SAAS,gBAAgB,MAAM,OAAO,CAAC;CAEvD,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA,IAAI,UAAU;GACZ,OAAO;EACT;EACA,SAAS,MAAM;CACjB;AACF"}
@@ -32,6 +32,56 @@ declare function getRuntimeContext(): RuntimeLike<never>;
32
32
  * ```
33
33
  */
34
34
  declare function provideRuntime<R = never>(runtime?: RuntimeLike<R>): void;
35
+ /**
36
+ * A dedicated, type-safe runtime context.
37
+ *
38
+ * Unlike the shared default context ({@link provideRuntime} / {@link getRuntime},
39
+ * which is untyped — `RuntimeLike<never>`), each call to `createRuntimeContext`
40
+ * mints its *own* context instance carrying a fully-typed `RuntimeLike<R>`. A
41
+ * library whose whole point is a service-typed runtime (e.g.
42
+ * `ManagedRuntime<ClientView, never>`) can therefore expose its own context
43
+ * without colliding with the default context or with other libraries, and
44
+ * `get()` returns the precise `RuntimeLike<R>` rather than `RuntimeLike<never>`.
45
+ */
46
+ interface RuntimeContext<R> {
47
+ /**
48
+ * Provide the runtime to the component subtree. Like `setContext`, this must
49
+ * be called during component initialisation.
50
+ */
51
+ readonly provide: (runtime: RuntimeLike<R>) => void;
52
+ /**
53
+ * Retrieve the runtime. Must be called during component initialisation.
54
+ * Throws an actionable error when no ancestor called `provide`.
55
+ */
56
+ readonly get: () => RuntimeLike<R>;
57
+ }
58
+ /**
59
+ * Create a typed runtime context, built on Svelte's
60
+ * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) (one
61
+ * context instance per call).
62
+ *
63
+ * @param name - an optional label used in the "runtime not provided" error, to
64
+ * help downstream users find the missing `provide` call.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // library.ts — a library exposing a service-typed runtime
69
+ * import { createRuntimeContext } from '@thomasfosterau/effect-svelte';
70
+ * import type { ManagedRuntime } from 'effect';
71
+ * import type { ClientView } from './client.js';
72
+ *
73
+ * export const ClientContext =
74
+ * createRuntimeContext<ClientView>('@legation/svelte client');
75
+ * ```
76
+ * ```svelte
77
+ * <script lang="ts">
78
+ * // Root.svelte
79
+ * import { ClientContext } from './library.js';
80
+ * ClientContext.provide(myClientRuntime);
81
+ * </script>
82
+ * ```
83
+ */
84
+ declare function createRuntimeContext<R>(name?: string): RuntimeContext<R>;
35
85
  //#endregion
36
- export { getRuntimeContext, provideRuntime };
86
+ export { type RuntimeContext, createRuntimeContext, getRuntimeContext, provideRuntime };
37
87
  //# sourceMappingURL=context.svelte.d.ts.map
@@ -47,7 +47,49 @@ function getRuntimeContext() {
47
47
  function provideRuntime(runtime = defaultSvelteRuntime) {
48
48
  provideRuntimeInternal(runtime);
49
49
  }
50
+ /**
51
+ * Create a typed runtime context, built on Svelte's
52
+ * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) (one
53
+ * context instance per call).
54
+ *
55
+ * @param name - an optional label used in the "runtime not provided" error, to
56
+ * help downstream users find the missing `provide` call.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * // library.ts — a library exposing a service-typed runtime
61
+ * import { createRuntimeContext } from '@thomasfosterau/effect-svelte';
62
+ * import type { ManagedRuntime } from 'effect';
63
+ * import type { ClientView } from './client.js';
64
+ *
65
+ * export const ClientContext =
66
+ * createRuntimeContext<ClientView>('@legation/svelte client');
67
+ * ```
68
+ * ```svelte
69
+ * <script lang="ts">
70
+ * // Root.svelte
71
+ * import { ClientContext } from './library.js';
72
+ * ClientContext.provide(myClientRuntime);
73
+ * <\/script>
74
+ * ```
75
+ */
76
+ function createRuntimeContext(name) {
77
+ const [get, set] = createContext();
78
+ const label = name === void 0 ? "this runtime context" : `the "${name}" runtime context`;
79
+ return {
80
+ provide(runtime) {
81
+ set(runtime);
82
+ },
83
+ get() {
84
+ try {
85
+ return get();
86
+ } catch {
87
+ throw new Error(`effect-svelte: No Effect runtime found for ${label}. Call its \`provide(runtime)\` in a parent component during initialisation.`);
88
+ }
89
+ }
90
+ };
91
+ }
50
92
  //#endregion
51
- export { getRuntimeContext, provideRuntime };
93
+ export { createRuntimeContext, getRuntimeContext, provideRuntime };
52
94
 
53
95
  //# sourceMappingURL=context.svelte.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.svelte.js","names":[],"sources":["../src/context.svelte.ts"],"sourcesContent":["import { createContext } from \"svelte\";\nimport { defaultSvelteRuntime } from \"./runtime.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\n\n/**\n * Type-safe context for Effect runtime management.\n * Uses Svelte 5's createContext API for better type safety and developer experience.\n *\n * The context accepts either a plain services `Context.Context` (e.g. `Context.empty()`)\n * or a `ManagedRuntime` (e.g. `SvelteRuntime.defaultRuntime` / `SvelteRuntime.extend(...)`).\n * Users must ensure their runtime provides all required services for the effects they run.\n */\nconst [getRuntime, provideRuntimeInternal] = createContext<RuntimeLike<never>>();\n\n/**\n * Retrieves the Effect runtime from Svelte context.\n *\n * @returns The runtime provided via provideRuntime\n * @throws Will throw if called outside of a component with a provided runtime\n */\nfunction getRuntimeContext(): RuntimeLike<never> {\n const runtime = getRuntime();\n if (runtime === undefined) {\n throw new Error(\n \"effect-svelte: No Effect runtime found in context. \" +\n \"Make sure you have called provideRuntime() in a parent component.\",\n );\n }\n return runtime;\n}\n\n/**\n * Provides an Effect runtime to the Svelte context tree.\n * This should be called at the root of your component tree, typically in your root layout.\n *\n * The default runtime (SvelteRuntime.defaultRuntime) is recommended for most applications.\n * For applications requiring custom services, extend the runtime using SvelteRuntime.extend().\n *\n * @param runtime - The Effect runtime to provide. Defaults to SvelteRuntime.defaultRuntime\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { provideRuntime, SvelteRuntime } from '@thomasfosterau/effect-svelte';\n *\n * // Use default runtime\n * provideRuntime();\n *\n * // Or provide a custom runtime\n * const customRuntime = SvelteRuntime.extend(myCustomLayer);\n * provideRuntime(customRuntime);\n * </script>\n * ```\n */\nfunction provideRuntime<R = never>(\n runtime: RuntimeLike<R> = defaultSvelteRuntime as RuntimeLike<R>,\n): void {\n provideRuntimeInternal(runtime as RuntimeLike<never>);\n}\n\nexport { getRuntimeContext as getRuntime, provideRuntime };\nexport type { RuntimeLike };\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,CAAC,YAAY,0BAA0B,cAAkC;;;;;;;AAQ/E,SAAS,oBAAwC;CAC/C,MAAM,UAAU,WAAW;CAC3B,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,sHAEF;CAEF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,eACP,UAA0B,sBACpB;CACN,uBAAuB,OAA6B;AACtD"}
1
+ {"version":3,"file":"context.svelte.js","names":[],"sources":["../src/context.svelte.ts"],"sourcesContent":["import { createContext } from \"svelte\";\nimport { defaultSvelteRuntime } from \"./runtime.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\n\n/**\n * Type-safe context for Effect runtime management.\n * Uses Svelte 5's createContext API for better type safety and developer experience.\n *\n * The context accepts either a plain services `Context.Context` (e.g. `Context.empty()`)\n * or a `ManagedRuntime` (e.g. `SvelteRuntime.defaultRuntime` / `SvelteRuntime.extend(...)`).\n * Users must ensure their runtime provides all required services for the effects they run.\n */\nconst [getRuntime, provideRuntimeInternal] = createContext<RuntimeLike<never>>();\n\n/**\n * Retrieves the Effect runtime from Svelte context.\n *\n * @returns The runtime provided via provideRuntime\n * @throws Will throw if called outside of a component with a provided runtime\n */\nfunction getRuntimeContext(): RuntimeLike<never> {\n const runtime = getRuntime();\n if (runtime === undefined) {\n throw new Error(\n \"effect-svelte: No Effect runtime found in context. \" +\n \"Make sure you have called provideRuntime() in a parent component.\",\n );\n }\n return runtime;\n}\n\n/**\n * Provides an Effect runtime to the Svelte context tree.\n * This should be called at the root of your component tree, typically in your root layout.\n *\n * The default runtime (SvelteRuntime.defaultRuntime) is recommended for most applications.\n * For applications requiring custom services, extend the runtime using SvelteRuntime.extend().\n *\n * @param runtime - The Effect runtime to provide. Defaults to SvelteRuntime.defaultRuntime\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { provideRuntime, SvelteRuntime } from '@thomasfosterau/effect-svelte';\n *\n * // Use default runtime\n * provideRuntime();\n *\n * // Or provide a custom runtime\n * const customRuntime = SvelteRuntime.extend(myCustomLayer);\n * provideRuntime(customRuntime);\n * </script>\n * ```\n */\nfunction provideRuntime<R = never>(\n runtime: RuntimeLike<R> = defaultSvelteRuntime as RuntimeLike<R>,\n): void {\n provideRuntimeInternal(runtime as RuntimeLike<never>);\n}\n\n/**\n * A dedicated, type-safe runtime context.\n *\n * Unlike the shared default context ({@link provideRuntime} / {@link getRuntime},\n * which is untyped — `RuntimeLike<never>`), each call to `createRuntimeContext`\n * mints its *own* context instance carrying a fully-typed `RuntimeLike<R>`. A\n * library whose whole point is a service-typed runtime (e.g.\n * `ManagedRuntime<ClientView, never>`) can therefore expose its own context\n * without colliding with the default context or with other libraries, and\n * `get()` returns the precise `RuntimeLike<R>` rather than `RuntimeLike<never>`.\n */\ninterface RuntimeContext<R> {\n /**\n * Provide the runtime to the component subtree. Like `setContext`, this must\n * be called during component initialisation.\n */\n readonly provide: (runtime: RuntimeLike<R>) => void;\n\n /**\n * Retrieve the runtime. Must be called during component initialisation.\n * Throws an actionable error when no ancestor called `provide`.\n */\n readonly get: () => RuntimeLike<R>;\n}\n\n/**\n * Create a typed runtime context, built on Svelte's\n * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) (one\n * context instance per call).\n *\n * @param name - an optional label used in the \"runtime not provided\" error, to\n * help downstream users find the missing `provide` call.\n *\n * @example\n * ```ts\n * // library.ts — a library exposing a service-typed runtime\n * import { createRuntimeContext } from '@thomasfosterau/effect-svelte';\n * import type { ManagedRuntime } from 'effect';\n * import type { ClientView } from './client.js';\n *\n * export const ClientContext =\n * createRuntimeContext<ClientView>('@legation/svelte client');\n * ```\n * ```svelte\n * <script lang=\"ts\">\n * // Root.svelte\n * import { ClientContext } from './library.js';\n * ClientContext.provide(myClientRuntime);\n * </script>\n * ```\n */\nfunction createRuntimeContext<R>(name?: string): RuntimeContext<R> {\n const [get, set] = createContext<RuntimeLike<R>>();\n const label = name === undefined ? \"this runtime context\" : `the \"${name}\" runtime context`;\n\n return {\n provide(runtime) {\n set(runtime);\n },\n get() {\n try {\n return get();\n } catch {\n throw new Error(\n `effect-svelte: No Effect runtime found for ${label}. ` +\n \"Call its `provide(runtime)` in a parent component during initialisation.\",\n );\n }\n },\n };\n}\n\nexport { createRuntimeContext, getRuntimeContext as getRuntime, provideRuntime };\nexport type { RuntimeContext, RuntimeLike };\n"],"mappings":";;;;;;;;;;;AAYA,MAAM,CAAC,YAAY,0BAA0B,cAAkC;;;;;;;AAQ/E,SAAS,oBAAwC;CAC/C,MAAM,UAAU,WAAW;CAC3B,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,sHAEF;CAEF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAS,eACP,UAA0B,sBACpB;CACN,uBAAuB,OAA6B;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAS,qBAAwB,MAAkC;CACjE,MAAM,CAAC,KAAK,OAAO,cAA8B;CACjD,MAAM,QAAQ,SAAS,KAAA,IAAY,yBAAyB,QAAQ,KAAK;CAEzE,OAAO;EACL,QAAQ,SAAS;GACf,IAAI,OAAO;EACb;EACA,MAAM;GACJ,IAAI;IACF,OAAO,IAAI;GACb,QAAQ;IACN,MAAM,IAAI,MACR,8CAA8C,MAAM,6EAEtD;GACF;EACF;CACF;AACF"}
@@ -1,7 +1,25 @@
1
- import { Effect } from "effect";
1
+ import { RuntimeLike } from "./internal/run.js";
2
+ import { Duration, Effect } from "effect";
2
3
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
4
 
4
5
  //#region src/derived.svelte.d.ts
6
+ interface UseDerivedOptions<R = never> {
7
+ /**
8
+ * Debounce re-runs by this duration: when a dependency changes, wait this
9
+ * long before running, and if another change arrives within the window,
10
+ * restart the wait. The previous value stays visible during the window (the
11
+ * result only flips to `waiting` once the run actually starts).
12
+ *
13
+ * Accepts any Effect `Duration.Input` (e.g. `"300 millis"`, `500`,
14
+ * `Duration.seconds(1)`).
15
+ */
16
+ readonly debounce?: Duration.Input;
17
+ /**
18
+ * Run against this runtime instead of the ambient context. When provided, the
19
+ * effect's `R` is constrained to what the runtime provides.
20
+ */
21
+ readonly runtime?: RuntimeLike<R>;
22
+ }
5
23
  interface DerivedReturn<A, E> {
6
24
  /**
7
25
  * The current result. While re-running after a dependency change, the
@@ -14,30 +32,32 @@ interface DerivedReturn<A, E> {
14
32
  * The function parameter is called inside $effect, so it automatically tracks dependencies.
15
33
  * When dependencies change, the previous fiber is interrupted and a new one starts.
16
34
  *
35
+ * Pass `{ debounce }` to coalesce rapid dependency changes (e.g. a search box):
36
+ * interrupting the previous fiber cancels its pending debounce sleep, so only
37
+ * the last change in a burst actually runs.
38
+ *
17
39
  * @example
18
40
  * ```svelte
19
41
  * <script lang="ts">
20
42
  * import { useDerived, AsyncResult } from '@thomasfosterau/effect-svelte';
21
43
  * import { Effect } from 'effect';
22
44
  *
23
- * let userId = $state(1);
45
+ * let query = $state('');
24
46
  *
25
- * const user = useDerived(() =>
26
- * Effect.gen(function* () {
27
- * const response = yield* fetchUser(userId);
28
- * return response;
29
- * })
47
+ * const results = useDerived(
48
+ * () => search(query),
49
+ * { debounce: '300 millis' },
30
50
  * );
31
51
  * </script>
32
52
  *
33
- * <button onclick={() => userId++}>Next User</button>
53
+ * <input bind:value={query} />
34
54
  *
35
- * {#if AsyncResult.isSuccess(user.current)}
36
- * <p>User: {user.current.value.name}</p>
55
+ * {#if AsyncResult.isSuccess(results.current)}
56
+ * <ul>{#each results.current.value as r}<li>{r}</li>{/each}</ul>
37
57
  * {/if}
38
58
  * ```
39
59
  */
40
- declare function useDerived<A, E, R>(fn: () => Effect.Effect<A, E, R>): DerivedReturn<A, E>;
60
+ declare function useDerived<A, E, R>(fn: () => Effect.Effect<A, E, R>, options?: UseDerivedOptions<R>): DerivedReturn<A, E>;
41
61
  //#endregion
42
- export { DerivedReturn, useDerived };
62
+ export { DerivedReturn, UseDerivedOptions, useDerived };
43
63
  //# sourceMappingURL=derived.svelte.d.ts.map
@@ -8,38 +8,48 @@ import { Effect, Fiber } from "effect";
8
8
  * The function parameter is called inside $effect, so it automatically tracks dependencies.
9
9
  * When dependencies change, the previous fiber is interrupted and a new one starts.
10
10
  *
11
+ * Pass `{ debounce }` to coalesce rapid dependency changes (e.g. a search box):
12
+ * interrupting the previous fiber cancels its pending debounce sleep, so only
13
+ * the last change in a burst actually runs.
14
+ *
11
15
  * @example
12
16
  * ```svelte
13
17
  * <script lang="ts">
14
18
  * import { useDerived, AsyncResult } from '@thomasfosterau/effect-svelte';
15
19
  * import { Effect } from 'effect';
16
20
  *
17
- * let userId = $state(1);
21
+ * let query = $state('');
18
22
  *
19
- * const user = useDerived(() =>
20
- * Effect.gen(function* () {
21
- * const response = yield* fetchUser(userId);
22
- * return response;
23
- * })
23
+ * const results = useDerived(
24
+ * () => search(query),
25
+ * { debounce: '300 millis' },
24
26
  * );
25
27
  * <\/script>
26
28
  *
27
- * <button onclick={() => userId++}>Next User</button>
29
+ * <input bind:value={query} />
28
30
  *
29
- * {#if AsyncResult.isSuccess(user.current)}
30
- * <p>User: {user.current.value.name}</p>
31
+ * {#if AsyncResult.isSuccess(results.current)}
32
+ * <ul>{#each results.current.value as r}<li>{r}</li>{/each}</ul>
31
33
  * {/if}
32
34
  * ```
33
35
  */
34
- function useDerived(fn) {
35
- const runtime = getRuntimeContext();
36
+ function useDerived(fn, options = {}) {
37
+ const { debounce } = options;
38
+ const runtime = options.runtime ?? getRuntimeContext();
36
39
  const state = makeResult();
37
40
  let currentFiber = null;
38
41
  $effect(() => {
39
42
  if (currentFiber !== null) interruptFiber(currentFiber);
40
43
  const effect = fn();
41
- state.startWaiting();
42
- const runningFiber = runFork(runtime)(effect);
44
+ let runningFiber;
45
+ if (debounce === void 0) {
46
+ state.startWaiting();
47
+ runningFiber = runFork(runtime)(effect);
48
+ } else runningFiber = runFork(runtime)(Effect.gen(function* () {
49
+ yield* Effect.sleep(debounce);
50
+ yield* Effect.sync(() => state.startWaiting());
51
+ return yield* effect;
52
+ }));
43
53
  currentFiber = runningFiber;
44
54
  runFork(runtime)(Effect.gen(function* () {
45
55
  const exit = yield* Fiber.await(runningFiber);
@@ -1 +1 @@
1
- {"version":3,"file":"derived.svelte.js","names":["getRuntime"],"sources":["../src/derived.svelte.ts"],"sourcesContent":["import { Effect, Fiber } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./internal/run.js\";\nimport { makeResult } from \"./internal/result.svelte.js\";\n\nexport interface DerivedReturn<A, E> {\n /**\n * The current result. While re-running after a dependency change, the\n * previous value is kept as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n}\n\n/**\n * Re-run an effect whenever reactive dependencies change.\n * The function parameter is called inside $effect, so it automatically tracks dependencies.\n * When dependencies change, the previous fiber is interrupted and a new one starts.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useDerived, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * let userId = $state(1);\n *\n * const user = useDerived(() =>\n * Effect.gen(function* () {\n * const response = yield* fetchUser(userId);\n * return response;\n * })\n * );\n * </script>\n *\n * <button onclick={() => userId++}>Next User</button>\n *\n * {#if AsyncResult.isSuccess(user.current)}\n * <p>User: {user.current.value.name}</p>\n * {/if}\n * ```\n */\nexport function useDerived<A, E, R>(fn: () => Effect.Effect<A, E, R>): DerivedReturn<A, E> {\n const runtime = getRuntime() as RuntimeLike<R>;\n\n const state = makeResult<A, E>();\n let currentFiber: Fiber.Fiber<A, E> | null = null;\n\n $effect(() => {\n // Interrupt previous fiber if it exists\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n }\n\n // Get the effect (this tracks reactive dependencies)\n const effect = fn();\n\n // Mark as waiting, preserving the previous value if there is one.\n state.startWaiting();\n\n // Run the effect and capture the fiber reference\n const runningFiber = runFork(runtime)(effect);\n currentFiber = runningFiber;\n\n // Settle the result when the fiber completes, unless deps changed first.\n runFork(runtime)(\n Effect.gen(function* () {\n const exit = yield* Fiber.await(runningFiber);\n if (currentFiber === runningFiber) {\n state.settle(exit);\n currentFiber = null;\n }\n }),\n );\n\n // Cleanup: interrupt the fiber when dependencies change or component unmounts\n return () => {\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n currentFiber = null;\n }\n };\n });\n\n return {\n get current() {\n return state.current;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,WAAoB,IAAuD;CACzF,MAAM,UAAUA,kBAAW;CAE3B,MAAM,QAAQ,WAAiB;CAC/B,IAAI,eAAyC;CAE7C,cAAc;EAEZ,IAAI,iBAAiB,MACnB,eAAe,YAAY;EAI7B,MAAM,SAAS,GAAG;EAGlB,MAAM,aAAa;EAGnB,MAAM,eAAe,QAAQ,OAAO,EAAE,MAAM;EAC5C,eAAe;EAGf,QAAQ,OAAO,EACb,OAAO,IAAI,aAAa;GACtB,MAAM,OAAO,OAAO,MAAM,MAAM,YAAY;GAC5C,IAAI,iBAAiB,cAAc;IACjC,MAAM,OAAO,IAAI;IACjB,eAAe;GACjB;EACF,CAAC,CACH;EAGA,aAAa;GACX,IAAI,iBAAiB,MAAM;IACzB,eAAe,YAAY;IAC3B,eAAe;GACjB;EACF;CACF,CAAC;CAED,OAAO,EACL,IAAI,UAAU;EACZ,OAAO,MAAM;CACf,EACF;AACF"}
1
+ {"version":3,"file":"derived.svelte.js","names":["getRuntime"],"sources":["../src/derived.svelte.ts"],"sourcesContent":["import { Duration, Effect, Fiber } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./internal/run.js\";\nimport { makeResult } from \"./internal/result.svelte.js\";\n\nexport interface UseDerivedOptions<R = never> {\n /**\n * Debounce re-runs by this duration: when a dependency changes, wait this\n * long before running, and if another change arrives within the window,\n * restart the wait. The previous value stays visible during the window (the\n * result only flips to `waiting` once the run actually starts).\n *\n * Accepts any Effect `Duration.Input` (e.g. `\"300 millis\"`, `500`,\n * `Duration.seconds(1)`).\n */\n readonly debounce?: Duration.Input;\n\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * effect's `R` is constrained to what the runtime provides.\n */\n readonly runtime?: RuntimeLike<R>;\n}\n\nexport interface DerivedReturn<A, E> {\n /**\n * The current result. While re-running after a dependency change, the\n * previous value is kept as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n}\n\n/**\n * Re-run an effect whenever reactive dependencies change.\n * The function parameter is called inside $effect, so it automatically tracks dependencies.\n * When dependencies change, the previous fiber is interrupted and a new one starts.\n *\n * Pass `{ debounce }` to coalesce rapid dependency changes (e.g. a search box):\n * interrupting the previous fiber cancels its pending debounce sleep, so only\n * the last change in a burst actually runs.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useDerived, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * let query = $state('');\n *\n * const results = useDerived(\n * () => search(query),\n * { debounce: '300 millis' },\n * );\n * </script>\n *\n * <input bind:value={query} />\n *\n * {#if AsyncResult.isSuccess(results.current)}\n * <ul>{#each results.current.value as r}<li>{r}</li>{/each}</ul>\n * {/if}\n * ```\n */\nexport function useDerived<A, E, R>(\n fn: () => Effect.Effect<A, E, R>,\n options: UseDerivedOptions<R> = {},\n): DerivedReturn<A, E> {\n const { debounce } = options;\n const runtime = options.runtime ?? (getRuntime() as RuntimeLike<R>);\n\n const state = makeResult<A, E>();\n let currentFiber: Fiber.Fiber<A, E> | null = null;\n\n $effect(() => {\n // Interrupt previous fiber if it exists (this also cancels a pending\n // debounce sleep, which is what makes debouncing work).\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n }\n\n // Get the effect (this tracks reactive dependencies).\n const effect = fn();\n\n let runningFiber: Fiber.Fiber<A, E>;\n if (debounce === undefined) {\n // Mark as waiting, preserving the previous value if there is one.\n state.startWaiting();\n runningFiber = runFork(runtime)(effect);\n } else {\n // Wait out the debounce window first; only then flip to waiting and run,\n // so the previous value stays visible until the run actually begins.\n runningFiber = runFork(runtime)(\n Effect.gen(function* () {\n yield* Effect.sleep(debounce);\n yield* Effect.sync(() => state.startWaiting());\n return yield* effect;\n }),\n );\n }\n currentFiber = runningFiber;\n\n // Settle the result when the fiber completes, unless deps changed first.\n runFork(runtime)(\n Effect.gen(function* () {\n const exit = yield* Fiber.await(runningFiber);\n if (currentFiber === runningFiber) {\n state.settle(exit);\n currentFiber = null;\n }\n }),\n );\n\n // Cleanup: interrupt the fiber when dependencies change or component unmounts\n return () => {\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n currentFiber = null;\n }\n };\n });\n\n return {\n get current() {\n return state.current;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,SAAgB,WACd,IACA,UAAgC,CAAC,GACZ;CACrB,MAAM,EAAE,aAAa;CACrB,MAAM,UAAU,QAAQ,WAAYA,kBAAW;CAE/C,MAAM,QAAQ,WAAiB;CAC/B,IAAI,eAAyC;CAE7C,cAAc;EAGZ,IAAI,iBAAiB,MACnB,eAAe,YAAY;EAI7B,MAAM,SAAS,GAAG;EAElB,IAAI;EACJ,IAAI,aAAa,KAAA,GAAW;GAE1B,MAAM,aAAa;GACnB,eAAe,QAAQ,OAAO,EAAE,MAAM;EACxC,OAGE,eAAe,QAAQ,OAAO,EAC5B,OAAO,IAAI,aAAa;GACtB,OAAO,OAAO,MAAM,QAAQ;GAC5B,OAAO,OAAO,WAAW,MAAM,aAAa,CAAC;GAC7C,OAAO,OAAO;EAChB,CAAC,CACH;EAEF,eAAe;EAGf,QAAQ,OAAO,EACb,OAAO,IAAI,aAAa;GACtB,MAAM,OAAO,OAAO,MAAM,MAAM,YAAY;GAC5C,IAAI,iBAAiB,cAAc;IACjC,MAAM,OAAO,IAAI;IACjB,eAAe;GACjB;EACF,CAAC,CACH;EAGA,aAAa;GACX,IAAI,iBAAiB,MAAM;IACzB,eAAe,YAAY;IAC3B,eAAe;GACjB;EACF;CACF,CAAC;CAED,OAAO,EACL,IAAI,UAAU;EACZ,OAAO,MAAM;CACf,EACF;AACF"}
@@ -1,14 +1,20 @@
1
+ import { RuntimeLike } from "./internal/run.js";
1
2
  import { Effect } from "effect";
2
3
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
4
 
4
5
  //#region src/effect.svelte.d.ts
5
- interface UseEffectOptions {
6
+ interface UseEffectOptions<R = never> {
6
7
  /**
7
8
  * If true, the effect will run immediately on mount.
8
9
  * If false, the effect will only run when manually triggered via run().
9
10
  * @default false
10
11
  */
11
12
  immediate?: boolean;
13
+ /**
14
+ * Run against this runtime instead of the ambient context. When provided, the
15
+ * effect's `R` is constrained to what the runtime provides.
16
+ */
17
+ runtime?: RuntimeLike<R>;
12
18
  }
13
19
  interface UseEffectReturn<A, E> {
14
20
  /**
@@ -24,6 +30,19 @@ interface UseEffectReturn<A, E> {
24
30
  * Interrupt the currently running effect
25
31
  */
26
32
  interrupt: () => void;
33
+ /**
34
+ * Optimistically overwrite the current value with a non-waiting `Success`,
35
+ * without running the effect. Accepts a value or an updater function that
36
+ * receives the current value (`undefined` if there is none yet). Useful for
37
+ * local cache writes after a mutation, reconciled by the next `run`.
38
+ */
39
+ setData: (updater: A | ((previous: A | undefined) => A)) => void;
40
+ /**
41
+ * `true` while a previous value is being shown but a re-run is in flight
42
+ * (stale-while-revalidate) — i.e. the result is `waiting` and still holds a
43
+ * value.
44
+ */
45
+ readonly isStale: boolean;
27
46
  }
28
47
  /**
29
48
  * Run a single Effect and track its result state.
@@ -45,7 +64,7 @@ interface UseEffectReturn<A, E> {
45
64
  * {/if}
46
65
  * ```
47
66
  */
48
- declare function useEffect<A, E, R>(effect: Effect.Effect<A, E, R>, options?: UseEffectOptions): UseEffectReturn<A, E>;
67
+ declare function useEffect<A, E, R>(effect: Effect.Effect<A, E, R>, options?: UseEffectOptions<R>): UseEffectReturn<A, E>;
49
68
  //#endregion
50
69
  export { UseEffectOptions, UseEffectReturn, useEffect };
51
70
  //# sourceMappingURL=effect.svelte.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { getRuntimeContext } from "./context.svelte.js";
2
2
  import { interruptFiber, runFork } from "./internal/run.js";
3
3
  import { makeResult } from "./internal/result.svelte.js";
4
- import { Effect, Fiber } from "effect";
4
+ import { Effect, Fiber, Option } from "effect";
5
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
5
6
  //#region src/effect.svelte.ts
6
7
  /**
7
8
  * Run a single Effect and track its result state.
@@ -25,7 +26,7 @@ import { Effect, Fiber } from "effect";
25
26
  */
26
27
  function useEffect(effect, options = {}) {
27
28
  const { immediate = false } = options;
28
- const runtime = getRuntimeContext();
29
+ const runtime = options.runtime ?? getRuntimeContext();
29
30
  const state = makeResult();
30
31
  let currentFiber = null;
31
32
  const run = () => {
@@ -54,12 +55,21 @@ function useEffect(effect, options = {}) {
54
55
  interrupt();
55
56
  };
56
57
  });
58
+ const setData = (updater) => {
59
+ const previous = Option.getOrUndefined(AsyncResult.value(state.current));
60
+ const next = typeof updater === "function" ? updater(previous) : updater;
61
+ state.succeed(next);
62
+ };
57
63
  return {
58
64
  get current() {
59
65
  return state.current;
60
66
  },
61
67
  run,
62
- interrupt
68
+ interrupt,
69
+ setData,
70
+ get isStale() {
71
+ return AsyncResult.isWaiting(state.current) && Option.isSome(AsyncResult.value(state.current));
72
+ }
63
73
  };
64
74
  }
65
75
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"effect.svelte.js","names":["getRuntime"],"sources":["../src/effect.svelte.ts"],"sourcesContent":["import { Effect, Fiber } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./internal/run.js\";\nimport { makeResult } from \"./internal/result.svelte.js\";\n\nexport interface UseEffectOptions {\n /**\n * If true, the effect will run immediately on mount.\n * If false, the effect will only run when manually triggered via run().\n * @default false\n */\n immediate?: boolean;\n}\n\nexport interface UseEffectReturn<A, E> {\n /**\n * The current result state of the effect. Re-running preserves the previous\n * value as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * Manually trigger execution of the effect\n */\n run: () => void;\n\n /**\n * Interrupt the currently running effect\n */\n interrupt: () => void;\n}\n\n/**\n * Run a single Effect and track its result state.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useEffect, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const fetchData = useEffect(\n * Effect.delay(Effect.succeed(42), '1 second'),\n * { immediate: true }\n * );\n * </script>\n *\n * {#if AsyncResult.isSuccess(fetchData.current)}\n * <p>Result: {fetchData.current.value}</p>\n * {/if}\n * ```\n */\nexport function useEffect<A, E, R>(\n effect: Effect.Effect<A, E, R>,\n options: UseEffectOptions = {},\n): UseEffectReturn<A, E> {\n const { immediate = false } = options;\n const runtime = getRuntime() as RuntimeLike<R>;\n\n const state = makeResult<A, E>();\n let currentFiber: Fiber.Fiber<A, E> | null = null;\n\n const run = () => {\n // Interrupt any existing fiber\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n }\n\n // Mark as waiting, preserving the previous value if there is one.\n state.startWaiting();\n\n // Run the effect and capture the fiber reference\n const runningFiber = runFork(runtime)(effect);\n currentFiber = runningFiber;\n\n // Settle the result when the fiber completes, unless a newer run replaced it.\n runFork(runtime)(\n Effect.gen(function* () {\n const exit = yield* Fiber.await(runningFiber);\n if (currentFiber === runningFiber) {\n state.settle(exit);\n currentFiber = null;\n }\n }),\n );\n };\n\n const interrupt = () => {\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n currentFiber = null;\n // Return to the initial state so the effect can be run again cleanly.\n state.reset();\n }\n };\n\n // Set up effect lifecycle\n $effect(() => {\n if (immediate) {\n run();\n }\n\n // Cleanup: interrupt any running fiber on unmount\n return () => {\n interrupt();\n };\n });\n\n return {\n get current() {\n return state.current;\n },\n run,\n interrupt,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,SAAgB,UACd,QACA,UAA4B,CAAC,GACN;CACvB,MAAM,EAAE,YAAY,UAAU;CAC9B,MAAM,UAAUA,kBAAW;CAE3B,MAAM,QAAQ,WAAiB;CAC/B,IAAI,eAAyC;CAE7C,MAAM,YAAY;EAEhB,IAAI,iBAAiB,MACnB,eAAe,YAAY;EAI7B,MAAM,aAAa;EAGnB,MAAM,eAAe,QAAQ,OAAO,EAAE,MAAM;EAC5C,eAAe;EAGf,QAAQ,OAAO,EACb,OAAO,IAAI,aAAa;GACtB,MAAM,OAAO,OAAO,MAAM,MAAM,YAAY;GAC5C,IAAI,iBAAiB,cAAc;IACjC,MAAM,OAAO,IAAI;IACjB,eAAe;GACjB;EACF,CAAC,CACH;CACF;CAEA,MAAM,kBAAkB;EACtB,IAAI,iBAAiB,MAAM;GACzB,eAAe,YAAY;GAC3B,eAAe;GAEf,MAAM,MAAM;EACd;CACF;CAGA,cAAc;EACZ,IAAI,WACF,IAAI;EAIN,aAAa;GACX,UAAU;EACZ;CACF,CAAC;CAED,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"effect.svelte.js","names":["getRuntime"],"sources":["../src/effect.svelte.ts"],"sourcesContent":["import { Effect, Fiber, Option } from \"effect\";\nimport * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./internal/run.js\";\nimport { makeResult } from \"./internal/result.svelte.js\";\n\nexport interface UseEffectOptions<R = never> {\n /**\n * If true, the effect will run immediately on mount.\n * If false, the effect will only run when manually triggered via run().\n * @default false\n */\n immediate?: boolean;\n\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * effect's `R` is constrained to what the runtime provides.\n */\n runtime?: RuntimeLike<R>;\n}\n\nexport interface UseEffectReturn<A, E> {\n /**\n * The current result state of the effect. Re-running preserves the previous\n * value as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * Manually trigger execution of the effect\n */\n run: () => void;\n\n /**\n * Interrupt the currently running effect\n */\n interrupt: () => void;\n\n /**\n * Optimistically overwrite the current value with a non-waiting `Success`,\n * without running the effect. Accepts a value or an updater function that\n * receives the current value (`undefined` if there is none yet). Useful for\n * local cache writes after a mutation, reconciled by the next `run`.\n */\n setData: (updater: A | ((previous: A | undefined) => A)) => void;\n\n /**\n * `true` while a previous value is being shown but a re-run is in flight\n * (stale-while-revalidate) — i.e. the result is `waiting` and still holds a\n * value.\n */\n readonly isStale: boolean;\n}\n\n/**\n * Run a single Effect and track its result state.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useEffect, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const fetchData = useEffect(\n * Effect.delay(Effect.succeed(42), '1 second'),\n * { immediate: true }\n * );\n * </script>\n *\n * {#if AsyncResult.isSuccess(fetchData.current)}\n * <p>Result: {fetchData.current.value}</p>\n * {/if}\n * ```\n */\nexport function useEffect<A, E, R>(\n effect: Effect.Effect<A, E, R>,\n options: UseEffectOptions<R> = {},\n): UseEffectReturn<A, E> {\n const { immediate = false } = options;\n const runtime = options.runtime ?? (getRuntime() as RuntimeLike<R>);\n\n const state = makeResult<A, E>();\n let currentFiber: Fiber.Fiber<A, E> | null = null;\n\n const run = () => {\n // Interrupt any existing fiber\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n }\n\n // Mark as waiting, preserving the previous value if there is one.\n state.startWaiting();\n\n // Run the effect and capture the fiber reference\n const runningFiber = runFork(runtime)(effect);\n currentFiber = runningFiber;\n\n // Settle the result when the fiber completes, unless a newer run replaced it.\n runFork(runtime)(\n Effect.gen(function* () {\n const exit = yield* Fiber.await(runningFiber);\n if (currentFiber === runningFiber) {\n state.settle(exit);\n currentFiber = null;\n }\n }),\n );\n };\n\n const interrupt = () => {\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n currentFiber = null;\n // Return to the initial state so the effect can be run again cleanly.\n state.reset();\n }\n };\n\n // Set up effect lifecycle\n $effect(() => {\n if (immediate) {\n run();\n }\n\n // Cleanup: interrupt any running fiber on unmount\n return () => {\n interrupt();\n };\n });\n\n const setData = (updater: A | ((previous: A | undefined) => A)): void => {\n const previous = Option.getOrUndefined(AsyncResult.value(state.current));\n const next =\n typeof updater === \"function\"\n ? (updater as (previous: A | undefined) => A)(previous)\n : updater;\n state.succeed(next);\n };\n\n return {\n get current() {\n return state.current;\n },\n run,\n interrupt,\n setData,\n get isStale() {\n return (\n AsyncResult.isWaiting(state.current) && Option.isSome(AsyncResult.value(state.current))\n );\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA,SAAgB,UACd,QACA,UAA+B,CAAC,GACT;CACvB,MAAM,EAAE,YAAY,UAAU;CAC9B,MAAM,UAAU,QAAQ,WAAYA,kBAAW;CAE/C,MAAM,QAAQ,WAAiB;CAC/B,IAAI,eAAyC;CAE7C,MAAM,YAAY;EAEhB,IAAI,iBAAiB,MACnB,eAAe,YAAY;EAI7B,MAAM,aAAa;EAGnB,MAAM,eAAe,QAAQ,OAAO,EAAE,MAAM;EAC5C,eAAe;EAGf,QAAQ,OAAO,EACb,OAAO,IAAI,aAAa;GACtB,MAAM,OAAO,OAAO,MAAM,MAAM,YAAY;GAC5C,IAAI,iBAAiB,cAAc;IACjC,MAAM,OAAO,IAAI;IACjB,eAAe;GACjB;EACF,CAAC,CACH;CACF;CAEA,MAAM,kBAAkB;EACtB,IAAI,iBAAiB,MAAM;GACzB,eAAe,YAAY;GAC3B,eAAe;GAEf,MAAM,MAAM;EACd;CACF;CAGA,cAAc;EACZ,IAAI,WACF,IAAI;EAIN,aAAa;GACX,UAAU;EACZ;CACF,CAAC;CAED,MAAM,WAAW,YAAwD;EACvE,MAAM,WAAW,OAAO,eAAe,YAAY,MAAM,MAAM,OAAO,CAAC;EACvE,MAAM,OACJ,OAAO,YAAY,aACd,QAA2C,QAAQ,IACpD;EACN,MAAM,QAAQ,IAAI;CACpB;CAEA,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA;EACA;EACA;EACA,IAAI,UAAU;GACZ,OACE,YAAY,UAAU,MAAM,OAAO,KAAK,OAAO,OAAO,YAAY,MAAM,MAAM,OAAO,CAAC;EAE1F;CACF;AACF"}
package/dist/index.d.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import { RuntimeLike } from "./internal/run.js";
2
- import { getRuntimeContext, provideRuntime } from "./context.svelte.js";
2
+ import { RuntimeContext, createRuntimeContext, getRuntimeContext, provideRuntime } from "./context.svelte.js";
3
3
  import { SvelteRuntime, defaultSvelteRuntime } from "./runtime.js";
4
4
  import { useScope, useScopeCallback } from "./scope.svelte.js";
5
5
  import { UseEffectOptions, UseEffectReturn, useEffect } from "./effect.svelte.js";
6
- import { UseStreamReturn, useStream } from "./stream.svelte.js";
7
- import { DerivedReturn, useDerived } from "./derived.svelte.js";
8
- import { UseQueryReturn, useQuery } from "./query.svelte.js";
9
- import { UsePubSubReturn, UseSubscriptionRefReturn, usePubSub, useSubscriptionRef } from "./subscription.svelte.js";
10
- import { ReactiveMutationReturn, ReactiveQueryReturn, ReactiveStreamReturn, reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys } from "./reactivity.svelte.js";
6
+ import { UseStreamOptions, UseStreamReturn, useStream } from "./stream.svelte.js";
7
+ import { UseLiveStreamOptions, UseLiveStreamReturn, useLiveStream } from "./live-stream.svelte.js";
8
+ import { DerivedReturn, UseDerivedOptions, useDerived } from "./derived.svelte.js";
9
+ import { UseQueryOptions, UseQueryReturn, useQuery } from "./query.svelte.js";
10
+ import { MutationCallbacks } from "./internal/mutation.js";
11
+ import { UseMutationOptions, UseMutationReturn, useMutation } from "./mutation.svelte.js";
12
+ import { UseQueryPromiseOptions, UseQueryPromiseReturn, useQueryPromise } from "./await.svelte.js";
13
+ import { UsePubSubOptions, UsePubSubReturn, UseSubscriptionRefOptions, UseSubscriptionRefReturn, usePubSub, useSubscriptionRef } from "./subscription.svelte.js";
14
+ import { UseWritableRefOptions, UseWritableRefReturn, useWritableRef } from "./writable-ref.svelte.js";
15
+ import { ReactiveMutationReturn, ReactiveOptions, ReactiveQueryReturn, ReactiveStreamReturn, reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys } from "./reactivity.svelte.js";
11
16
  import { SignalEmitter } from "./emitter.js";
12
17
  import { Store_d_exports } from "./Store.js";
13
18
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
14
- export { AsyncResult, type DerivedReturn, type ReactiveMutationReturn, type ReactiveQueryReturn, type ReactiveStreamReturn, type RuntimeLike, SignalEmitter, Store_d_exports as Store, SvelteRuntime, type UseEffectOptions, type UseEffectReturn, type UsePubSubReturn, type UseQueryReturn, type UseStreamReturn, type UseSubscriptionRefReturn, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, usePubSub, useQuery, useScope, useScopeCallback, useStream, useSubscriptionRef };
19
+ export { AsyncResult, type DerivedReturn, type MutationCallbacks, type ReactiveMutationReturn, type ReactiveOptions, type ReactiveQueryReturn, type ReactiveStreamReturn, type RuntimeContext, type RuntimeLike, SignalEmitter, Store_d_exports as Store, SvelteRuntime, type UseDerivedOptions, type UseEffectOptions, type UseEffectReturn, type UseLiveStreamOptions, type UseLiveStreamReturn, type UseMutationOptions, type UseMutationReturn, type UsePubSubOptions, type UsePubSubReturn, type UseQueryOptions, type UseQueryPromiseOptions, type UseQueryPromiseReturn, type UseQueryReturn, type UseStreamOptions, type UseStreamReturn, type UseSubscriptionRefOptions, type UseSubscriptionRefReturn, type UseWritableRefOptions, type UseWritableRefReturn, createRuntimeContext, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, useLiveStream, useMutation, usePubSub, useQuery, useQueryPromise, useScope, useScopeCallback, useStream, useSubscriptionRef, useWritableRef };
package/dist/index.js CHANGED
@@ -1,13 +1,17 @@
1
1
  import { SvelteRuntime, defaultSvelteRuntime } from "./runtime.js";
2
- import { getRuntimeContext, provideRuntime } from "./context.svelte.js";
2
+ import { createRuntimeContext, getRuntimeContext, provideRuntime } from "./context.svelte.js";
3
3
  import { useScope, useScopeCallback } from "./scope.svelte.js";
4
4
  import { useEffect } from "./effect.svelte.js";
5
5
  import { useStream } from "./stream.svelte.js";
6
+ import { useLiveStream } from "./live-stream.svelte.js";
6
7
  import { useDerived } from "./derived.svelte.js";
7
8
  import { useQuery } from "./query.svelte.js";
9
+ import { useMutation } from "./mutation.svelte.js";
10
+ import { useQueryPromise } from "./await.svelte.js";
8
11
  import { usePubSub, useSubscriptionRef } from "./subscription.svelte.js";
12
+ import { useWritableRef } from "./writable-ref.svelte.js";
9
13
  import { reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys } from "./reactivity.svelte.js";
10
14
  import { SignalEmitter } from "./emitter.js";
11
15
  import { Store_exports } from "./Store.js";
12
16
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
13
- export { AsyncResult, SignalEmitter, Store_exports as Store, SvelteRuntime, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, usePubSub, useQuery, useScope, useScopeCallback, useStream, useSubscriptionRef };
17
+ export { AsyncResult, SignalEmitter, Store_exports as Store, SvelteRuntime, createRuntimeContext, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, useLiveStream, useMutation, usePubSub, useQuery, useQueryPromise, useScope, useScopeCallback, useStream, useSubscriptionRef, useWritableRef };
@@ -0,0 +1,52 @@
1
+ import { Cause, Option } from "effect";
2
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
+ //#region src/internal/await.ts
4
+ /**
5
+ * Convert an {@link AsyncResult.AsyncResult} into a `Promise`, so Effect-backed
6
+ * state can drive Svelte 5's native async (`{#await}` blocks and
7
+ * `<svelte:boundary>` suspense), which both consume a real thenable.
8
+ *
9
+ * Mapping:
10
+ * - **Failure** → a rejected promise (with the typed error `E` when present,
11
+ * otherwise the squashed cause). Checked first, so a failure that carries a
12
+ * previous success still surfaces the error in the `{:catch}` block.
13
+ * - **Holds a value** (a `Success`, including a waiting one during
14
+ * stale-while-revalidate) → a resolved promise, so the UI keeps showing data
15
+ * while a refresh is in flight.
16
+ * - **Initial / waiting with no value** → a perpetually-pending promise, so the
17
+ * `{#await}` loading branch (or the boundary's `pending` snippet) shows.
18
+ *
19
+ * Callers should memoise the result (e.g. via `$derived`) so a fresh pending
20
+ * promise is not produced on unrelated re-renders — a new promise identity is
21
+ * only wanted when the underlying result transitions.
22
+ */
23
+ function resultToPromise(result) {
24
+ if (AsyncResult.isFailure(result)) {
25
+ const cause = AsyncResult.cause(result);
26
+ return rejected(Option.isSome(cause) ? Option.match(Cause.findErrorOption(cause.value), {
27
+ onNone: () => Cause.squash(cause.value),
28
+ onSome: (error) => error
29
+ }) : /* @__PURE__ */ new Error("AsyncResult failure with no cause"));
30
+ }
31
+ const value = AsyncResult.value(result);
32
+ if (Option.isSome(value)) return Promise.resolve(value.value);
33
+ return new Promise(() => {});
34
+ }
35
+ /**
36
+ * A rejected promise whose rejection is pre-observed with a no-op handler.
37
+ *
38
+ * Failures are surfaced as rejected promises from a `$derived`; a reactive
39
+ * re-render can produce (and then discard) such a promise before anything
40
+ * `await`s it. The no-op `catch` keeps that discarded value from tripping
41
+ * Node's `unhandledRejection` warning, while the returned promise still rejects
42
+ * for every real consumer (the `catch` runs on a separate branch).
43
+ */
44
+ function rejected(reason) {
45
+ const promise = Promise.reject(reason);
46
+ promise.catch(() => {});
47
+ return promise;
48
+ }
49
+ //#endregion
50
+ export { resultToPromise };
51
+
52
+ //# sourceMappingURL=await.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"await.js","names":[],"sources":["../../src/internal/await.ts"],"sourcesContent":["import { Cause, Option } from \"effect\";\nimport * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\n\n/**\n * Convert an {@link AsyncResult.AsyncResult} into a `Promise`, so Effect-backed\n * state can drive Svelte 5's native async (`{#await}` blocks and\n * `<svelte:boundary>` suspense), which both consume a real thenable.\n *\n * Mapping:\n * - **Failure** → a rejected promise (with the typed error `E` when present,\n * otherwise the squashed cause). Checked first, so a failure that carries a\n * previous success still surfaces the error in the `{:catch}` block.\n * - **Holds a value** (a `Success`, including a waiting one during\n * stale-while-revalidate) → a resolved promise, so the UI keeps showing data\n * while a refresh is in flight.\n * - **Initial / waiting with no value** → a perpetually-pending promise, so the\n * `{#await}` loading branch (or the boundary's `pending` snippet) shows.\n *\n * Callers should memoise the result (e.g. via `$derived`) so a fresh pending\n * promise is not produced on unrelated re-renders — a new promise identity is\n * only wanted when the underlying result transitions.\n */\nexport function resultToPromise<A, E>(result: AsyncResult.AsyncResult<A, E>): Promise<A> {\n if (AsyncResult.isFailure(result)) {\n const cause = AsyncResult.cause(result);\n const reason = Option.isSome(cause)\n ? Option.match(Cause.findErrorOption(cause.value), {\n onNone: () => Cause.squash(cause.value),\n onSome: (error) => error,\n })\n : new Error(\"AsyncResult failure with no cause\");\n return rejected(reason);\n }\n\n const value = AsyncResult.value(result);\n if (Option.isSome(value)) return Promise.resolve(value.value);\n\n // Initial / waiting with no value yet: stay pending.\n return new Promise<A>(() => {});\n}\n\n/**\n * A rejected promise whose rejection is pre-observed with a no-op handler.\n *\n * Failures are surfaced as rejected promises from a `$derived`; a reactive\n * re-render can produce (and then discard) such a promise before anything\n * `await`s it. The no-op `catch` keeps that discarded value from tripping\n * Node's `unhandledRejection` warning, while the returned promise still rejects\n * for every real consumer (the `catch` runs on a separate branch).\n */\nfunction rejected<A>(reason: unknown): Promise<A> {\n const promise = Promise.reject<A>(reason);\n promise.catch(() => {});\n return promise;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,gBAAsB,QAAmD;CACvF,IAAI,YAAY,UAAU,MAAM,GAAG;EACjC,MAAM,QAAQ,YAAY,MAAM,MAAM;EAOtC,OAAO,SANQ,OAAO,OAAO,KAAK,IAC9B,OAAO,MAAM,MAAM,gBAAgB,MAAM,KAAK,GAAG;GAC/C,cAAc,MAAM,OAAO,MAAM,KAAK;GACtC,SAAS,UAAU;EACrB,CAAC,oBACD,IAAI,MAAM,mCAAmC,CAC3B;CACxB;CAEA,MAAM,QAAQ,YAAY,MAAM,MAAM;CACtC,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO,QAAQ,QAAQ,MAAM,KAAK;CAG5D,OAAO,IAAI,cAAiB,CAAC,CAAC;AAChC;;;;;;;;;;AAWA,SAAS,SAAY,QAA6B;CAChD,MAAM,UAAU,QAAQ,OAAU,MAAM;CACxC,QAAQ,YAAY,CAAC,CAAC;CACtB,OAAO;AACT"}