@thomasfosterau/effect-svelte 0.1.0 → 0.2.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.
@@ -0,0 +1,45 @@
1
+ import { Effect } from "effect";
2
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
+
4
+ //#region src/await.svelte.d.ts
5
+ interface UseQueryPromiseReturn<A, E> {
6
+ /** The underlying query result state (same as {@link useQuery}). */
7
+ readonly current: AsyncResult.AsyncResult<A, E>;
8
+ /**
9
+ * A `Promise` reflecting `current`, for use with Svelte's `{#await}` (or
10
+ * `<svelte:boundary>`). Its identity is stable until the result transitions:
11
+ * pending while loading, resolved with the (possibly stale) value, rejected
12
+ * on failure.
13
+ */
14
+ readonly promise: Promise<A>;
15
+ /** Re-run the effect. A new `promise` is produced as the result transitions. */
16
+ readonly refetch: () => void;
17
+ }
18
+ /**
19
+ * Run an effect and bridge its result to Svelte 5's native async, so you can
20
+ * render it with an `{#await}` block instead of `AsyncResult.match`.
21
+ *
22
+ * @example
23
+ * ```svelte
24
+ * <script lang="ts">
25
+ * import { useQueryPromise } from '@thomasfosterau/effect-svelte';
26
+ * import { Effect } from 'effect';
27
+ *
28
+ * const user = useQueryPromise(fetchUser(1));
29
+ * </script>
30
+ *
31
+ * {#await user.promise}
32
+ * <p>Loading…</p>
33
+ * {:then value}
34
+ * <p>{value.name}</p>
35
+ * {:catch error}
36
+ * <p>Failed: {error.message}</p>
37
+ * {/await}
38
+ *
39
+ * <button onclick={user.refetch}>Reload</button>
40
+ * ```
41
+ */
42
+ declare function useQueryPromise<A, E, R>(effect: Effect.Effect<A, E, R>): UseQueryPromiseReturn<A, E>;
43
+ //#endregion
44
+ export { UseQueryPromiseReturn, useQueryPromise };
45
+ //# sourceMappingURL=await.svelte.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { useQuery } from "./query.svelte.js";
2
+ import { resultToPromise } from "./internal/await.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) {
29
+ const query = useQuery(effect);
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 { useQuery } from \"./query.svelte.js\";\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): UseQueryPromiseReturn<A, E> {\n const query = useQuery(effect);\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,gBACd,QAC6B;CAC7B,MAAM,QAAQ,SAAS,MAAM;CAI7B,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"}
@@ -1,7 +1,19 @@
1
- import { Effect } from "effect";
1
+ import { Duration, Effect } from "effect";
2
2
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
3
3
 
4
4
  //#region src/derived.svelte.d.ts
5
+ interface UseDerivedOptions {
6
+ /**
7
+ * Debounce re-runs by this duration: when a dependency changes, wait this
8
+ * long before running, and if another change arrives within the window,
9
+ * restart the wait. The previous value stays visible during the window (the
10
+ * result only flips to `waiting` once the run actually starts).
11
+ *
12
+ * Accepts any Effect `Duration.Input` (e.g. `"300 millis"`, `500`,
13
+ * `Duration.seconds(1)`).
14
+ */
15
+ readonly debounce?: Duration.Input;
16
+ }
5
17
  interface DerivedReturn<A, E> {
6
18
  /**
7
19
  * The current result. While re-running after a dependency change, the
@@ -14,30 +26,32 @@ interface DerivedReturn<A, E> {
14
26
  * The function parameter is called inside $effect, so it automatically tracks dependencies.
15
27
  * When dependencies change, the previous fiber is interrupted and a new one starts.
16
28
  *
29
+ * Pass `{ debounce }` to coalesce rapid dependency changes (e.g. a search box):
30
+ * interrupting the previous fiber cancels its pending debounce sleep, so only
31
+ * the last change in a burst actually runs.
32
+ *
17
33
  * @example
18
34
  * ```svelte
19
35
  * <script lang="ts">
20
36
  * import { useDerived, AsyncResult } from '@thomasfosterau/effect-svelte';
21
37
  * import { Effect } from 'effect';
22
38
  *
23
- * let userId = $state(1);
39
+ * let query = $state('');
24
40
  *
25
- * const user = useDerived(() =>
26
- * Effect.gen(function* () {
27
- * const response = yield* fetchUser(userId);
28
- * return response;
29
- * })
41
+ * const results = useDerived(
42
+ * () => search(query),
43
+ * { debounce: '300 millis' },
30
44
  * );
31
45
  * </script>
32
46
  *
33
- * <button onclick={() => userId++}>Next User</button>
47
+ * <input bind:value={query} />
34
48
  *
35
- * {#if AsyncResult.isSuccess(user.current)}
36
- * <p>User: {user.current.value.name}</p>
49
+ * {#if AsyncResult.isSuccess(results.current)}
50
+ * <ul>{#each results.current.value as r}<li>{r}</li>{/each}</ul>
37
51
  * {/if}
38
52
  * ```
39
53
  */
40
- declare function useDerived<A, E, R>(fn: () => Effect.Effect<A, E, R>): DerivedReturn<A, E>;
54
+ declare function useDerived<A, E, R>(fn: () => Effect.Effect<A, E, R>, options?: UseDerivedOptions): DerivedReturn<A, E>;
41
55
  //#endregion
42
- export { DerivedReturn, useDerived };
56
+ export { DerivedReturn, UseDerivedOptions, useDerived };
43
57
  //# 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) {
36
+ function useDerived(fn, options = {}) {
37
+ const { debounce } = options;
35
38
  const 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 {\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\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 = {},\n): DerivedReturn<A, E> {\n const { debounce } = 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 $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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,WACd,IACA,UAA6B,CAAC,GACT;CACrB,MAAM,EAAE,aAAa;CACrB,MAAM,UAAUA,kBAAW;CAE3B,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"}
@@ -24,6 +24,19 @@ interface UseEffectReturn<A, E> {
24
24
  * Interrupt the currently running effect
25
25
  */
26
26
  interrupt: () => void;
27
+ /**
28
+ * Optimistically overwrite the current value with a non-waiting `Success`,
29
+ * without running the effect. Accepts a value or an updater function that
30
+ * receives the current value (`undefined` if there is none yet). Useful for
31
+ * local cache writes after a mutation, reconciled by the next `run`.
32
+ */
33
+ setData: (updater: A | ((previous: A | undefined) => A)) => void;
34
+ /**
35
+ * `true` while a previous value is being shown but a re-run is in flight
36
+ * (stale-while-revalidate) — i.e. the result is `waiting` and still holds a
37
+ * value.
38
+ */
39
+ readonly isStale: boolean;
27
40
  }
28
41
  /**
29
42
  * Run a single Effect and track its result state.
@@ -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.
@@ -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 {\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 * 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 = {},\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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,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,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
@@ -4,11 +4,15 @@ 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
6
  import { UseStreamReturn, useStream } from "./stream.svelte.js";
7
- import { DerivedReturn, useDerived } from "./derived.svelte.js";
7
+ import { DerivedReturn, UseDerivedOptions, useDerived } from "./derived.svelte.js";
8
8
  import { UseQueryReturn, useQuery } from "./query.svelte.js";
9
+ import { MutationCallbacks } from "./internal/mutation.js";
10
+ import { UseMutationReturn, useMutation } from "./mutation.svelte.js";
11
+ import { UseQueryPromiseReturn, useQueryPromise } from "./await.svelte.js";
9
12
  import { UsePubSubReturn, UseSubscriptionRefReturn, usePubSub, useSubscriptionRef } from "./subscription.svelte.js";
13
+ import { UseWritableRefReturn, useWritableRef } from "./writable-ref.svelte.js";
10
14
  import { ReactiveMutationReturn, ReactiveQueryReturn, ReactiveStreamReturn, reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys } from "./reactivity.svelte.js";
11
15
  import { SignalEmitter } from "./emitter.js";
12
16
  import { Store_d_exports } from "./Store.js";
13
17
  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 };
18
+ export { AsyncResult, type DerivedReturn, type MutationCallbacks, type ReactiveMutationReturn, type ReactiveQueryReturn, type ReactiveStreamReturn, type RuntimeLike, SignalEmitter, Store_d_exports as Store, SvelteRuntime, type UseDerivedOptions, type UseEffectOptions, type UseEffectReturn, type UseMutationReturn, type UsePubSubReturn, type UseQueryPromiseReturn, type UseQueryReturn, type UseStreamReturn, type UseSubscriptionRefReturn, type UseWritableRefReturn, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, useMutation, usePubSub, useQuery, useQueryPromise, useScope, useScopeCallback, useStream, useSubscriptionRef, useWritableRef };
package/dist/index.js CHANGED
@@ -5,9 +5,12 @@ import { useEffect } from "./effect.svelte.js";
5
5
  import { useStream } from "./stream.svelte.js";
6
6
  import { useDerived } from "./derived.svelte.js";
7
7
  import { useQuery } from "./query.svelte.js";
8
+ import { useMutation } from "./mutation.svelte.js";
9
+ import { useQueryPromise } from "./await.svelte.js";
8
10
  import { usePubSub, useSubscriptionRef } from "./subscription.svelte.js";
11
+ import { useWritableRef } from "./writable-ref.svelte.js";
9
12
  import { reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys } from "./reactivity.svelte.js";
10
13
  import { SignalEmitter } from "./emitter.js";
11
14
  import { Store_exports } from "./Store.js";
12
15
  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 };
16
+ export { AsyncResult, SignalEmitter, Store_exports as Store, SvelteRuntime, defaultSvelteRuntime, getRuntimeContext as getRuntime, provideRuntime, reactiveMutation, reactiveQuery, reactiveStream, useDerived, useEffect, useInvalidateKeys, useMutation, usePubSub, useQuery, useQueryPromise, useScope, useScopeCallback, useStream, useSubscriptionRef, useWritableRef };
@@ -0,0 +1,39 @@
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
+ if (Option.isSome(cause)) {
27
+ const error = Cause.findErrorOption(cause.value);
28
+ return Promise.reject(Option.isSome(error) ? error.value : Cause.squash(cause.value));
29
+ }
30
+ return Promise.reject(/* @__PURE__ */ new Error("AsyncResult failure with no cause"));
31
+ }
32
+ const value = AsyncResult.value(result);
33
+ if (Option.isSome(value)) return Promise.resolve(value.value);
34
+ return new Promise(() => {});
35
+ }
36
+ //#endregion
37
+ export { resultToPromise };
38
+
39
+ //# 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 if (Option.isSome(cause)) {\n const error = Cause.findErrorOption(cause.value);\n return Promise.reject(Option.isSome(error) ? error.value : Cause.squash(cause.value));\n }\n return Promise.reject(new Error(\"AsyncResult failure with no cause\"));\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,gBAAsB,QAAmD;CACvF,IAAI,YAAY,UAAU,MAAM,GAAG;EACjC,MAAM,QAAQ,YAAY,MAAM,MAAM;EACtC,IAAI,OAAO,OAAO,KAAK,GAAG;GACxB,MAAM,QAAQ,MAAM,gBAAgB,MAAM,KAAK;GAC/C,OAAO,QAAQ,OAAO,OAAO,OAAO,KAAK,IAAI,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAK,CAAC;EACtF;EACA,OAAO,QAAQ,uBAAO,IAAI,MAAM,mCAAmC,CAAC;CACtE;CAEA,MAAM,QAAQ,YAAY,MAAM,MAAM;CACtC,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO,QAAQ,QAAQ,MAAM,KAAK;CAG5D,OAAO,IAAI,cAAiB,CAAC,CAAC;AAChC"}
@@ -0,0 +1,38 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/mutation.d.ts
4
+ /**
5
+ * Lifecycle callbacks for a mutation, following TanStack Query conventions.
6
+ *
7
+ * `onMutate` runs synchronously before the effect starts and its return value
8
+ * is threaded through `onSuccess` / `onError` / `onSettled` as `context` — the
9
+ * idiomatic place to apply an optimistic update and capture a snapshot for
10
+ * rollback.
11
+ */
12
+ interface MutationCallbacks<A, E, I, C> {
13
+ /**
14
+ * Runs before the mutation effect. Apply an optimistic update here and return
15
+ * any value (e.g. a snapshot) to receive it back as `context` in the other
16
+ * callbacks — use it to roll back in `onError`.
17
+ */
18
+ readonly onMutate?: (input: I) => C;
19
+ /** Runs after the mutation succeeds, before invalidation. */
20
+ readonly onSuccess?: (value: A, input: I, context: C | undefined) => void;
21
+ /**
22
+ * Runs after the mutation fails with a typed error. Pure defects (dies)
23
+ * still settle `current` as a failure but do not invoke this callback, since
24
+ * there is no `E` value to hand back — inspect `current` for those.
25
+ */
26
+ readonly onError?: (error: E, input: I, context: C | undefined) => void;
27
+ /** Runs after `onSuccess` / `onError`, regardless of outcome. */
28
+ readonly onSettled?: (input: I, context: C | undefined) => void;
29
+ /**
30
+ * Reactivity keys to invalidate after a successful mutation (a static array
31
+ * or a function of the input). Requires the runtime to include
32
+ * `Reactivity.layer` from `effect/unstable/reactivity`.
33
+ */
34
+ readonly invalidates?: ReadonlyArray<unknown> | ((input: I) => ReadonlyArray<unknown>);
35
+ }
36
+ //#endregion
37
+ export { MutationCallbacks };
38
+ //# sourceMappingURL=mutation.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { interruptFiber, runFork } from "./run.js";
2
+ import { Cause, Effect, Exit, Fiber, Option } from "effect";
3
+ import { Reactivity } from "effect/unstable/reactivity";
4
+ //#region src/internal/mutation.ts
5
+ /**
6
+ * The orchestration half of {@link useMutation}, factored out of the rune
7
+ * wrapper so it can be unit-tested with a plain (non-rune) {@link ResultState}
8
+ * and a real runtime.
9
+ *
10
+ * Drives `state` through the stale-while-revalidate transitions and dispatches
11
+ * the {@link MutationCallbacks} from the settled `Exit`. Interrupted runs (a
12
+ * newer `mutate` replacing an older one, or teardown) are ignored so a
13
+ * superseded run never clobbers the latest result.
14
+ */
15
+ function makeMutation(runtime, state, fn, callbacks = {}) {
16
+ let currentFiber = null;
17
+ const mutate = (input) => {
18
+ if (currentFiber !== null) interruptFiber(currentFiber);
19
+ const context = callbacks.onMutate?.(input);
20
+ state.startWaiting();
21
+ const running = runFork(runtime)(fn(input));
22
+ currentFiber = running;
23
+ runFork(runtime)(Effect.gen(function* () {
24
+ const exit = yield* Fiber.await(running);
25
+ if (currentFiber !== running) return;
26
+ currentFiber = null;
27
+ if (Exit.isSuccess(exit)) {
28
+ state.settle(exit);
29
+ callbacks.onSuccess?.(exit.value, input, context);
30
+ const keys = typeof callbacks.invalidates === "function" ? callbacks.invalidates(input) : callbacks.invalidates;
31
+ if (keys !== void 0) runFork(runtime)(Reactivity.invalidate(keys));
32
+ callbacks.onSettled?.(input, context);
33
+ return;
34
+ }
35
+ if (Cause.hasInterrupts(exit.cause)) return;
36
+ state.settle(exit);
37
+ const error = Cause.findErrorOption(exit.cause);
38
+ if (Option.isSome(error)) callbacks.onError?.(error.value, input, context);
39
+ callbacks.onSettled?.(input, context);
40
+ }));
41
+ };
42
+ const interrupt = () => {
43
+ if (currentFiber !== null) {
44
+ interruptFiber(currentFiber);
45
+ currentFiber = null;
46
+ }
47
+ };
48
+ const reset = () => {
49
+ interrupt();
50
+ state.reset();
51
+ };
52
+ return {
53
+ mutate,
54
+ reset,
55
+ interrupt
56
+ };
57
+ }
58
+ //#endregion
59
+ export { makeMutation };
60
+
61
+ //# sourceMappingURL=mutation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutation.js","names":[],"sources":["../../src/internal/mutation.ts"],"sourcesContent":["import { Cause, Effect, Exit, Fiber, Option } from \"effect\";\nimport { Reactivity } from \"effect/unstable/reactivity\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./run.js\";\nimport type { ResultState } from \"./result.svelte.js\";\n\n/**\n * Lifecycle callbacks for a mutation, following TanStack Query conventions.\n *\n * `onMutate` runs synchronously before the effect starts and its return value\n * is threaded through `onSuccess` / `onError` / `onSettled` as `context` — the\n * idiomatic place to apply an optimistic update and capture a snapshot for\n * rollback.\n */\nexport interface MutationCallbacks<A, E, I, C> {\n /**\n * Runs before the mutation effect. Apply an optimistic update here and return\n * any value (e.g. a snapshot) to receive it back as `context` in the other\n * callbacks — use it to roll back in `onError`.\n */\n readonly onMutate?: (input: I) => C;\n /** Runs after the mutation succeeds, before invalidation. */\n readonly onSuccess?: (value: A, input: I, context: C | undefined) => void;\n /**\n * Runs after the mutation fails with a typed error. Pure defects (dies)\n * still settle `current` as a failure but do not invoke this callback, since\n * there is no `E` value to hand back — inspect `current` for those.\n */\n readonly onError?: (error: E, input: I, context: C | undefined) => void;\n /** Runs after `onSuccess` / `onError`, regardless of outcome. */\n readonly onSettled?: (input: I, context: C | undefined) => void;\n /**\n * Reactivity keys to invalidate after a successful mutation (a static array\n * or a function of the input). Requires the runtime to include\n * `Reactivity.layer` from `effect/unstable/reactivity`.\n */\n readonly invalidates?: ReadonlyArray<unknown> | ((input: I) => ReadonlyArray<unknown>);\n}\n\n/** The imperative surface of a mutation, independent of any rune state. */\nexport interface MutationCore<I> {\n /** Run the mutation for `input`, interrupting any in-flight run. */\n readonly mutate: (input: I) => void;\n /** Interrupt the in-flight run and reset the result to `Initial`. */\n readonly reset: () => void;\n /** Interrupt the in-flight run without resetting the result. */\n readonly interrupt: () => void;\n}\n\n/**\n * The orchestration half of {@link useMutation}, factored out of the rune\n * wrapper so it can be unit-tested with a plain (non-rune) {@link ResultState}\n * and a real runtime.\n *\n * Drives `state` through the stale-while-revalidate transitions and dispatches\n * the {@link MutationCallbacks} from the settled `Exit`. Interrupted runs (a\n * newer `mutate` replacing an older one, or teardown) are ignored so a\n * superseded run never clobbers the latest result.\n */\nexport function makeMutation<A, E, I, C, R>(\n runtime: RuntimeLike<R>,\n state: ResultState<A, E>,\n fn: (input: I) => Effect.Effect<A, E, R>,\n callbacks: MutationCallbacks<A, E, I, C> = {},\n): MutationCore<I> {\n let currentFiber: Fiber.Fiber<A, E> | null = null;\n\n const mutate = (input: I): void => {\n if (currentFiber !== null) interruptFiber(currentFiber);\n\n const context = callbacks.onMutate?.(input);\n state.startWaiting();\n\n const running = runFork(runtime)(fn(input));\n currentFiber = running;\n\n runFork(runtime)(\n Effect.gen(function* () {\n const exit = yield* Fiber.await(running);\n // A newer mutate (or teardown) has superseded this run.\n if (currentFiber !== running) return;\n currentFiber = null;\n\n if (Exit.isSuccess(exit)) {\n state.settle(exit);\n callbacks.onSuccess?.(exit.value, input, context);\n const keys =\n typeof callbacks.invalidates === \"function\"\n ? callbacks.invalidates(input)\n : callbacks.invalidates;\n if (keys !== undefined) {\n // Requires the runtime to provide Reactivity (documented contract).\n runFork(runtime as RuntimeLike<Reactivity.Reactivity>)(Reactivity.invalidate(keys));\n }\n callbacks.onSettled?.(input, context);\n return;\n }\n\n // Interruption is normal teardown; keep the prior state.\n if (Cause.hasInterrupts(exit.cause)) return;\n\n state.settle(exit);\n const error = Cause.findErrorOption(exit.cause);\n if (Option.isSome(error)) callbacks.onError?.(error.value, input, context);\n callbacks.onSettled?.(input, context);\n }),\n );\n };\n\n const interrupt = (): void => {\n if (currentFiber !== null) {\n interruptFiber(currentFiber);\n currentFiber = null;\n }\n };\n\n const reset = (): void => {\n interrupt();\n state.reset();\n };\n\n return { mutate, reset, interrupt };\n}\n"],"mappings":";;;;;;;;;;;;;;AA0DA,SAAgB,aACd,SACA,OACA,IACA,YAA2C,CAAC,GAC3B;CACjB,IAAI,eAAyC;CAE7C,MAAM,UAAU,UAAmB;EACjC,IAAI,iBAAiB,MAAM,eAAe,YAAY;EAEtD,MAAM,UAAU,UAAU,WAAW,KAAK;EAC1C,MAAM,aAAa;EAEnB,MAAM,UAAU,QAAQ,OAAO,EAAE,GAAG,KAAK,CAAC;EAC1C,eAAe;EAEf,QAAQ,OAAO,EACb,OAAO,IAAI,aAAa;GACtB,MAAM,OAAO,OAAO,MAAM,MAAM,OAAO;GAEvC,IAAI,iBAAiB,SAAS;GAC9B,eAAe;GAEf,IAAI,KAAK,UAAU,IAAI,GAAG;IACxB,MAAM,OAAO,IAAI;IACjB,UAAU,YAAY,KAAK,OAAO,OAAO,OAAO;IAChD,MAAM,OACJ,OAAO,UAAU,gBAAgB,aAC7B,UAAU,YAAY,KAAK,IAC3B,UAAU;IAChB,IAAI,SAAS,KAAA,GAEX,QAAQ,OAA6C,EAAE,WAAW,WAAW,IAAI,CAAC;IAEpF,UAAU,YAAY,OAAO,OAAO;IACpC;GACF;GAGA,IAAI,MAAM,cAAc,KAAK,KAAK,GAAG;GAErC,MAAM,OAAO,IAAI;GACjB,MAAM,QAAQ,MAAM,gBAAgB,KAAK,KAAK;GAC9C,IAAI,OAAO,OAAO,KAAK,GAAG,UAAU,UAAU,MAAM,OAAO,OAAO,OAAO;GACzE,UAAU,YAAY,OAAO,OAAO;EACtC,CAAC,CACH;CACF;CAEA,MAAM,kBAAwB;EAC5B,IAAI,iBAAiB,MAAM;GACzB,eAAe,YAAY;GAC3B,eAAe;EACjB;CACF;CAEA,MAAM,cAAoB;EACxB,UAAU;EACV,MAAM,MAAM;CACd;CAEA,OAAO;EAAE;EAAQ;EAAO;CAAU;AACpC"}
@@ -0,0 +1 @@
1
+ import { Cause, Exit } from "effect";
@@ -0,0 +1,21 @@
1
+ import { runFork } from "./run.js";
2
+ import { SubscriptionRef } from "effect";
3
+ //#region src/internal/writable.ts
4
+ /**
5
+ * Build a {@link RefWriter} for a `SubscriptionRef`, running writes on the
6
+ * given runtime.
7
+ */
8
+ function refWriter(runtime, ref) {
9
+ return {
10
+ set: (value) => {
11
+ runFork(runtime)(SubscriptionRef.set(ref, value));
12
+ },
13
+ update: (f) => {
14
+ runFork(runtime)(SubscriptionRef.update(ref, f));
15
+ }
16
+ };
17
+ }
18
+ //#endregion
19
+ export { refWriter };
20
+
21
+ //# sourceMappingURL=writable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writable.js","names":[],"sources":["../../src/internal/writable.ts"],"sourcesContent":["import { SubscriptionRef } from \"effect\";\nimport { runFork, type RuntimeLike } from \"./run.js\";\n\n/**\n * The imperative write half of a two-way `SubscriptionRef` binding, factored\n * out of the rune wrapper so the write semantics can be unit-tested without a\n * Svelte component context.\n *\n * Both `set` and `update` fork the write onto the runtime and return\n * immediately — the value the caller sees update is driven by the ref's\n * `changes` stream (or, in the rune wrapper, an optimistic local assignment).\n */\nexport interface RefWriter<A> {\n /** Replace the ref's value. */\n readonly set: (value: A) => void;\n /** Apply a function to the ref's current value. */\n readonly update: (f: (current: A) => A) => void;\n}\n\n/**\n * Build a {@link RefWriter} for a `SubscriptionRef`, running writes on the\n * given runtime.\n */\nexport function refWriter<A>(\n runtime: RuntimeLike<never>,\n ref: SubscriptionRef.SubscriptionRef<A>,\n): RefWriter<A> {\n return {\n set: (value) => {\n runFork(runtime)(SubscriptionRef.set(ref, value));\n },\n update: (f) => {\n runFork(runtime)(SubscriptionRef.update(ref, f));\n },\n };\n}\n"],"mappings":";;;;;;;AAuBA,SAAgB,UACd,SACA,KACc;CACd,OAAO;EACL,MAAM,UAAU;GACd,QAAQ,OAAO,EAAE,gBAAgB,IAAI,KAAK,KAAK,CAAC;EAClD;EACA,SAAS,MAAM;GACb,QAAQ,OAAO,EAAE,gBAAgB,OAAO,KAAK,CAAC,CAAC;EACjD;CACF;AACF"}
@@ -0,0 +1,53 @@
1
+ import { MutationCallbacks } from "./internal/mutation.js";
2
+ import { Effect } from "effect";
3
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
4
+
5
+ //#region src/mutation.svelte.d.ts
6
+ interface UseMutationReturn<A, E, I> {
7
+ /**
8
+ * The current result state of the mutation. Starts `Initial`; each `mutate`
9
+ * marks it waiting (preserving the previous value) and settles to
10
+ * `Success` / `Failure`.
11
+ */
12
+ readonly current: AsyncResult.AsyncResult<A, E>;
13
+ /** Run the mutation with `input`. A new call interrupts any in-flight run. */
14
+ readonly mutate: (input: I) => void;
15
+ /** Interrupt any in-flight run and reset `current` back to `Initial`. */
16
+ readonly reset: () => void;
17
+ }
18
+ /**
19
+ * A TanStack-Query-shaped mutation hook: run an effectful `fn(input)` on
20
+ * demand, track its {@link AsyncResult.AsyncResult}, and fire lifecycle
21
+ * callbacks.
22
+ *
23
+ * Optimistic updates follow the TanStack model — apply the change in `onMutate`
24
+ * and return a snapshot as `context`, then roll back in `onError`. `invalidates`
25
+ * re-runs any reactive queries depending on the given keys after success (this
26
+ * requires the runtime to include `Reactivity.layer`).
27
+ *
28
+ * @example
29
+ * ```svelte
30
+ * <script lang="ts">
31
+ * import { useMutation, AsyncResult } from '@thomasfosterau/effect-svelte';
32
+ * import { Effect } from 'effect';
33
+ *
34
+ * const addTodo = useMutation(
35
+ * (title: string) => saveTodo(title),
36
+ * {
37
+ * invalidates: ['todos'],
38
+ * onSuccess: (todo) => console.log('added', todo.id),
39
+ * },
40
+ * );
41
+ * </script>
42
+ *
43
+ * <button onclick={() => addTodo.mutate('Buy milk')}>Add</button>
44
+ *
45
+ * {#if AsyncResult.isWaiting(addTodo.current)}
46
+ * <p>Saving…</p>
47
+ * {/if}
48
+ * ```
49
+ */
50
+ declare function useMutation<A, E, I, R, C = unknown>(fn: (input: I) => Effect.Effect<A, E, R>, options?: MutationCallbacks<A, E, I, C>): UseMutationReturn<A, E, I>;
51
+ //#endregion
52
+ export { UseMutationReturn, useMutation };
53
+ //# sourceMappingURL=mutation.svelte.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { getRuntimeContext } from "./context.svelte.js";
2
+ import { makeResult } from "./internal/result.svelte.js";
3
+ import { makeMutation } from "./internal/mutation.js";
4
+ //#region src/mutation.svelte.ts
5
+ /**
6
+ * A TanStack-Query-shaped mutation hook: run an effectful `fn(input)` on
7
+ * demand, track its {@link AsyncResult.AsyncResult}, and fire lifecycle
8
+ * callbacks.
9
+ *
10
+ * Optimistic updates follow the TanStack model — apply the change in `onMutate`
11
+ * and return a snapshot as `context`, then roll back in `onError`. `invalidates`
12
+ * re-runs any reactive queries depending on the given keys after success (this
13
+ * requires the runtime to include `Reactivity.layer`).
14
+ *
15
+ * @example
16
+ * ```svelte
17
+ * <script lang="ts">
18
+ * import { useMutation, AsyncResult } from '@thomasfosterau/effect-svelte';
19
+ * import { Effect } from 'effect';
20
+ *
21
+ * const addTodo = useMutation(
22
+ * (title: string) => saveTodo(title),
23
+ * {
24
+ * invalidates: ['todos'],
25
+ * onSuccess: (todo) => console.log('added', todo.id),
26
+ * },
27
+ * );
28
+ * <\/script>
29
+ *
30
+ * <button onclick={() => addTodo.mutate('Buy milk')}>Add</button>
31
+ *
32
+ * {#if AsyncResult.isWaiting(addTodo.current)}
33
+ * <p>Saving…</p>
34
+ * {/if}
35
+ * ```
36
+ */
37
+ function useMutation(fn, options = {}) {
38
+ const runtime = getRuntimeContext();
39
+ const state = makeResult();
40
+ const core = makeMutation(runtime, state, fn, options);
41
+ $effect(() => () => core.interrupt());
42
+ return {
43
+ get current() {
44
+ return state.current;
45
+ },
46
+ mutate: core.mutate,
47
+ reset: core.reset
48
+ };
49
+ }
50
+ //#endregion
51
+ export { useMutation };
52
+
53
+ //# sourceMappingURL=mutation.svelte.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutation.svelte.js","names":["getRuntime"],"sources":["../src/mutation.svelte.ts"],"sourcesContent":["import type { Effect } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { makeMutation, type MutationCallbacks } from \"./internal/mutation.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\nimport { makeResult } from \"./internal/result.svelte.js\";\n\nexport type { MutationCallbacks } from \"./internal/mutation.js\";\n\nexport interface UseMutationReturn<A, E, I> {\n /**\n * The current result state of the mutation. Starts `Initial`; each `mutate`\n * marks it waiting (preserving the previous value) and settles to\n * `Success` / `Failure`.\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /** Run the mutation with `input`. A new call interrupts any in-flight run. */\n readonly mutate: (input: I) => void;\n\n /** Interrupt any in-flight run and reset `current` back to `Initial`. */\n readonly reset: () => void;\n}\n\n/**\n * A TanStack-Query-shaped mutation hook: run an effectful `fn(input)` on\n * demand, track its {@link AsyncResult.AsyncResult}, and fire lifecycle\n * callbacks.\n *\n * Optimistic updates follow the TanStack model — apply the change in `onMutate`\n * and return a snapshot as `context`, then roll back in `onError`. `invalidates`\n * re-runs any reactive queries depending on the given keys after success (this\n * requires the runtime to include `Reactivity.layer`).\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useMutation, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const addTodo = useMutation(\n * (title: string) => saveTodo(title),\n * {\n * invalidates: ['todos'],\n * onSuccess: (todo) => console.log('added', todo.id),\n * },\n * );\n * </script>\n *\n * <button onclick={() => addTodo.mutate('Buy milk')}>Add</button>\n *\n * {#if AsyncResult.isWaiting(addTodo.current)}\n * <p>Saving…</p>\n * {/if}\n * ```\n */\nexport function useMutation<A, E, I, R, C = unknown>(\n fn: (input: I) => Effect.Effect<A, E, R>,\n options: MutationCallbacks<A, E, I, C> = {},\n): UseMutationReturn<A, E, I> {\n const runtime = getRuntime() as RuntimeLike<R>;\n const state = makeResult<A, E>();\n const core = makeMutation(runtime, state, fn, options);\n\n // Interrupt any in-flight mutation when the component unmounts.\n $effect(() => () => core.interrupt());\n\n return {\n get current() {\n return state.current;\n },\n mutate: core.mutate,\n reset: core.reset,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,YACd,IACA,UAAyC,CAAC,GACd;CAC5B,MAAM,UAAUA,kBAAW;CAC3B,MAAM,QAAQ,WAAiB;CAC/B,MAAM,OAAO,aAAa,SAAS,OAAO,IAAI,OAAO;CAGrD,oBAAoB,KAAK,UAAU,CAAC;CAEpC,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA,QAAQ,KAAK;EACb,OAAO,KAAK;CACd;AACF"}
@@ -12,6 +12,17 @@ interface UseQueryReturn<A, E> {
12
12
  * Re-fetch the data by re-running the effect
13
13
  */
14
14
  refetch: () => void;
15
+ /**
16
+ * Optimistically overwrite the query's data locally without refetching.
17
+ * Accepts a value or an updater over the current value (`undefined` if none
18
+ * yet). The next `refetch` reconciles it with the server.
19
+ */
20
+ setData: (updater: A | ((previous: A | undefined) => A)) => void;
21
+ /**
22
+ * `true` while showing a previous value during a refetch
23
+ * (stale-while-revalidate).
24
+ */
25
+ readonly isStale: boolean;
15
26
  }
16
27
  /**
17
28
  * Query pattern with refetch capability.
@@ -32,7 +32,11 @@ function useQuery(effect) {
32
32
  get current() {
33
33
  return effectHook.current;
34
34
  },
35
- refetch: effectHook.run
35
+ refetch: effectHook.run,
36
+ setData: effectHook.setData,
37
+ get isStale() {
38
+ return effectHook.isStale;
39
+ }
36
40
  };
37
41
  }
38
42
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"query.svelte.js","names":[],"sources":["../src/query.svelte.ts"],"sourcesContent":["import type { Effect } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { useEffect } from \"./effect.svelte.js\";\n\nexport interface UseQueryReturn<A, E> {\n /**\n * The current result state of the query. Refetching preserves the previous\n * value as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * Re-fetch the data by re-running the effect\n */\n refetch: () => void;\n}\n\n/**\n * Query pattern with refetch capability.\n * Runs the effect immediately on mount and provides a refetch function.\n * Built on top of useEffect with immediate: true.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useQuery, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const query = useQuery(\n * Effect.gen(function* () {\n * const data = yield* fetchData();\n * return data;\n * })\n * );\n * </script>\n *\n * <button onclick={query.refetch}>Refresh</button>\n *\n * {#if AsyncResult.isSuccess(query.current)}\n * <p>Data: {JSON.stringify(query.current.value)}</p>\n * {/if}\n * ```\n */\nexport function useQuery<A, E, R>(effect: Effect.Effect<A, E, R>): UseQueryReturn<A, E> {\n const effectHook = useEffect(effect, { immediate: true });\n\n return {\n get current() {\n return effectHook.current;\n },\n refetch: effectHook.run,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,SAAkB,QAAsD;CACtF,MAAM,aAAa,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CAExD,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,WAAW;EACpB;EACA,SAAS,WAAW;CACtB;AACF"}
1
+ {"version":3,"file":"query.svelte.js","names":[],"sources":["../src/query.svelte.ts"],"sourcesContent":["import type { Effect } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { useEffect } from \"./effect.svelte.js\";\n\nexport interface UseQueryReturn<A, E> {\n /**\n * The current result state of the query. Refetching preserves the previous\n * value as a waiting `Success` (stale-while-revalidate).\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * Re-fetch the data by re-running the effect\n */\n refetch: () => void;\n\n /**\n * Optimistically overwrite the query's data locally without refetching.\n * Accepts a value or an updater over the current value (`undefined` if none\n * yet). The next `refetch` reconciles it with the server.\n */\n setData: (updater: A | ((previous: A | undefined) => A)) => void;\n\n /**\n * `true` while showing a previous value during a refetch\n * (stale-while-revalidate).\n */\n readonly isStale: boolean;\n}\n\n/**\n * Query pattern with refetch capability.\n * Runs the effect immediately on mount and provides a refetch function.\n * Built on top of useEffect with immediate: true.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useQuery, AsyncResult } from '@thomasfosterau/effect-svelte';\n * import { Effect } from 'effect';\n *\n * const query = useQuery(\n * Effect.gen(function* () {\n * const data = yield* fetchData();\n * return data;\n * })\n * );\n * </script>\n *\n * <button onclick={query.refetch}>Refresh</button>\n *\n * {#if AsyncResult.isSuccess(query.current)}\n * <p>Data: {JSON.stringify(query.current.value)}</p>\n * {/if}\n * ```\n */\nexport function useQuery<A, E, R>(effect: Effect.Effect<A, E, R>): UseQueryReturn<A, E> {\n const effectHook = useEffect(effect, { immediate: true });\n\n return {\n get current() {\n return effectHook.current;\n },\n refetch: effectHook.run,\n setData: effectHook.setData,\n get isStale() {\n return effectHook.isStale;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,SAAkB,QAAsD;CACtF,MAAM,aAAa,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CAExD,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,WAAW;EACpB;EACA,SAAS,WAAW;EACpB,SAAS,WAAW;EACpB,IAAI,UAAU;GACZ,OAAO,WAAW;EACpB;CACF;AACF"}
@@ -0,0 +1,48 @@
1
+ import { SubscriptionRef } from "effect";
2
+
3
+ //#region src/writable-ref.svelte.d.ts
4
+ interface UseWritableRefReturn<A> {
5
+ /**
6
+ * The current value of the `SubscriptionRef`. Reading tracks it reactively;
7
+ * assigning writes back to the ref, so this property is usable directly with
8
+ * Svelte's `bind:` directive (e.g. `bind:value={name.current}`).
9
+ */
10
+ current: A;
11
+ /** Replace the ref's value. */
12
+ readonly set: (value: A) => void;
13
+ /** Apply a function to the ref's current value. */
14
+ readonly update: (f: (current: A) => A) => void;
15
+ }
16
+ /**
17
+ * Two-way binding between an Effect `SubscriptionRef` and Svelte runes.
18
+ *
19
+ * Unlike {@link useSubscriptionRef} (read-only `{ current }`), this hook exposes
20
+ * a writable `current` — reading tracks the ref reactively and assigning writes
21
+ * back — so Effect-managed state can drive `bind:value` for shared form / app
22
+ * state. `set` and `update` are also provided for imperative writes.
23
+ *
24
+ * Local writes update `current` optimistically (so `bind:` does not glitch) and
25
+ * are reconciled by the ref's `changes` stream, which also delivers updates
26
+ * made elsewhere in the application.
27
+ *
28
+ * A `SubscriptionRef` is required (not a plain `Ref`): only a `SubscriptionRef`
29
+ * exposes the `changes` stream needed to observe external updates reactively.
30
+ *
31
+ * @example
32
+ * ```svelte
33
+ * <script lang="ts">
34
+ * import { useWritableRef } from '@thomasfosterau/effect-svelte';
35
+ * import { SubscriptionRef } from 'effect';
36
+ *
37
+ * // Assume nameRef is a SubscriptionRef<string> from shared app state
38
+ * const name = useWritableRef(nameRef);
39
+ * </script>
40
+ *
41
+ * <input bind:value={name.current} />
42
+ * <button onclick={() => name.set("")}>Clear</button>
43
+ * ```
44
+ */
45
+ declare function useWritableRef<A>(ref: SubscriptionRef.SubscriptionRef<A>): UseWritableRefReturn<A>;
46
+ //#endregion
47
+ export { UseWritableRefReturn, useWritableRef };
48
+ //# sourceMappingURL=writable-ref.svelte.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { getRuntimeContext } from "./context.svelte.js";
2
+ import { interruptFiber, runFork, runSync } from "./internal/run.js";
3
+ import { refWriter } from "./internal/writable.js";
4
+ import { Effect, Stream, SubscriptionRef } from "effect";
5
+ //#region src/writable-ref.svelte.ts
6
+ /**
7
+ * Two-way binding between an Effect `SubscriptionRef` and Svelte runes.
8
+ *
9
+ * Unlike {@link useSubscriptionRef} (read-only `{ current }`), this hook exposes
10
+ * a writable `current` — reading tracks the ref reactively and assigning writes
11
+ * back — so Effect-managed state can drive `bind:value` for shared form / app
12
+ * state. `set` and `update` are also provided for imperative writes.
13
+ *
14
+ * Local writes update `current` optimistically (so `bind:` does not glitch) and
15
+ * are reconciled by the ref's `changes` stream, which also delivers updates
16
+ * made elsewhere in the application.
17
+ *
18
+ * A `SubscriptionRef` is required (not a plain `Ref`): only a `SubscriptionRef`
19
+ * exposes the `changes` stream needed to observe external updates reactively.
20
+ *
21
+ * @example
22
+ * ```svelte
23
+ * <script lang="ts">
24
+ * import { useWritableRef } from '@thomasfosterau/effect-svelte';
25
+ * import { SubscriptionRef } from 'effect';
26
+ *
27
+ * // Assume nameRef is a SubscriptionRef<string> from shared app state
28
+ * const name = useWritableRef(nameRef);
29
+ * <\/script>
30
+ *
31
+ * <input bind:value={name.current} />
32
+ * <button onclick={() => name.set("")}>Clear</button>
33
+ * ```
34
+ */
35
+ function useWritableRef(ref) {
36
+ const runtime = getRuntimeContext();
37
+ let value = $state(runSync(runtime)(SubscriptionRef.get(ref)));
38
+ const writer = refWriter(runtime, ref);
39
+ const set = (next) => {
40
+ value = next;
41
+ writer.set(next);
42
+ };
43
+ const update = (f) => {
44
+ set(f(value));
45
+ };
46
+ $effect(() => {
47
+ const fiber = runFork(runtime)(Stream.runForEach(SubscriptionRef.changes(ref), (next) => Effect.sync(() => {
48
+ value = next;
49
+ })));
50
+ return () => interruptFiber(fiber);
51
+ });
52
+ return {
53
+ get current() {
54
+ return value;
55
+ },
56
+ set current(next) {
57
+ set(next);
58
+ },
59
+ set,
60
+ update
61
+ };
62
+ }
63
+ //#endregion
64
+ export { useWritableRef };
65
+
66
+ //# sourceMappingURL=writable-ref.svelte.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writable-ref.svelte.js","names":["getRuntime"],"sources":["../src/writable-ref.svelte.ts"],"sourcesContent":["import { Effect, Stream, SubscriptionRef } from \"effect\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { interruptFiber, runFork, runSync, type RuntimeLike } from \"./internal/run.js\";\nimport { refWriter } from \"./internal/writable.js\";\n\nexport interface UseWritableRefReturn<A> {\n /**\n * The current value of the `SubscriptionRef`. Reading tracks it reactively;\n * assigning writes back to the ref, so this property is usable directly with\n * Svelte's `bind:` directive (e.g. `bind:value={name.current}`).\n */\n current: A;\n\n /** Replace the ref's value. */\n readonly set: (value: A) => void;\n\n /** Apply a function to the ref's current value. */\n readonly update: (f: (current: A) => A) => void;\n}\n\n/**\n * Two-way binding between an Effect `SubscriptionRef` and Svelte runes.\n *\n * Unlike {@link useSubscriptionRef} (read-only `{ current }`), this hook exposes\n * a writable `current` — reading tracks the ref reactively and assigning writes\n * back — so Effect-managed state can drive `bind:value` for shared form / app\n * state. `set` and `update` are also provided for imperative writes.\n *\n * Local writes update `current` optimistically (so `bind:` does not glitch) and\n * are reconciled by the ref's `changes` stream, which also delivers updates\n * made elsewhere in the application.\n *\n * A `SubscriptionRef` is required (not a plain `Ref`): only a `SubscriptionRef`\n * exposes the `changes` stream needed to observe external updates reactively.\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useWritableRef } from '@thomasfosterau/effect-svelte';\n * import { SubscriptionRef } from 'effect';\n *\n * // Assume nameRef is a SubscriptionRef<string> from shared app state\n * const name = useWritableRef(nameRef);\n * </script>\n *\n * <input bind:value={name.current} />\n * <button onclick={() => name.set(\"\")}>Clear</button>\n * ```\n */\nexport function useWritableRef<A>(\n ref: SubscriptionRef.SubscriptionRef<A>,\n): UseWritableRefReturn<A> {\n const runtime = getRuntime() as RuntimeLike<never>;\n\n // Read the initial value synchronously so it is available before (and during\n // SSR, instead of) the subscription.\n let value = $state(runSync(runtime)(SubscriptionRef.get(ref)));\n\n const writer = refWriter(runtime, ref);\n\n const set = (next: A): void => {\n // Optimistic local update so `bind:value` reflects the change immediately,\n // then persist to the ref (whose `changes` stream reconciles the value).\n value = next;\n writer.set(next);\n };\n\n const update = (f: (current: A) => A): void => {\n set(f(value));\n };\n\n // Keep `value` in sync with updates made anywhere else in the app. The\n // `changes` stream re-emits the current value on subscribe, so no update is\n // missed between the synchronous initial read and the subscription starting.\n $effect(() => {\n const fiber = runFork(runtime)(\n Stream.runForEach(SubscriptionRef.changes(ref), (next) =>\n Effect.sync(() => {\n value = next;\n }),\n ),\n );\n return () => interruptFiber(fiber);\n });\n\n return {\n get current() {\n return value;\n },\n set current(next: A) {\n set(next);\n },\n set,\n update,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,eACd,KACyB;CACzB,MAAM,UAAUA,kBAAW;CAI3B,IAAI,QAAQ,OAAO,QAAQ,OAAO,EAAE,gBAAgB,IAAI,GAAG,CAAC,CAAC;CAE7D,MAAM,SAAS,UAAU,SAAS,GAAG;CAErC,MAAM,OAAO,SAAkB;EAG7B,QAAQ;EACR,OAAO,IAAI,IAAI;CACjB;CAEA,MAAM,UAAU,MAA+B;EAC7C,IAAI,EAAE,KAAK,CAAC;CACd;CAKA,cAAc;EACZ,MAAM,QAAQ,QAAQ,OAAO,EAC3B,OAAO,WAAW,gBAAgB,QAAQ,GAAG,IAAI,SAC/C,OAAO,WAAW;GAChB,QAAQ;EACV,CAAC,CACH,CACF;EACA,aAAa,eAAe,KAAK;CACnC,CAAC;CAED,OAAO;EACL,IAAI,UAAU;GACZ,OAAO;EACT;EACA,IAAI,QAAQ,MAAS;GACnB,IAAI,IAAI;EACV;EACA;EACA;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thomasfosterau/effect-svelte",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Effect integration for Svelte 5 runes",
5
5
  "keywords": [
6
6
  "effect",