@thomasfosterau/effect-svelte 0.3.0 → 0.3.2
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRuntimeContext } from "./context.svelte.js";
|
|
2
2
|
import { resultToPromise } from "./internal/await.js";
|
|
3
3
|
import { makeLiveStream } from "./internal/live-stream.js";
|
|
4
|
+
import { untrack } from "svelte";
|
|
4
5
|
import { Option } from "effect";
|
|
5
6
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
6
7
|
//#region src/live-stream.svelte.ts
|
|
@@ -16,7 +17,7 @@ function makeLiveResult(seed) {
|
|
|
16
17
|
return current;
|
|
17
18
|
},
|
|
18
19
|
startWaiting() {
|
|
19
|
-
current = AsyncResult.waiting(current);
|
|
20
|
+
current = AsyncResult.waiting(untrack(() => current));
|
|
20
21
|
},
|
|
21
22
|
emit(value) {
|
|
22
23
|
current = AsyncResult.success(value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-stream.svelte.js","names":["getRuntime"],"sources":["../src/live-stream.svelte.ts"],"sourcesContent":["import { Option, type Stream } from \"effect\";\nimport * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { resultToPromise } from \"./internal/await.js\";\nimport { makeLiveStream, type LiveResultState } from \"./internal/live-stream.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\n\nexport interface UseLiveStreamOptions<A, R> {\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * stream's `R` is constrained to what the runtime provides (a service-typed\n * `ManagedRuntime` gives a fully-checked `R`).\n */\n readonly runtime?: RuntimeLike<R>;\n\n /**\n * A synchronous seed value. When provided, `current` starts as a non-waiting\n * `Success(initial)` (and `promise` starts already-resolved) instead of\n * `Initial`, so server renders and the first client render show the data the\n * server already had — the first real emission then replaces it. This is the\n * SSR seam: without a synchronous seed, SSR output and the first client render\n * would show empty state and hydration would mismatch.\n */\n readonly initial?: A;\n}\n\nexport interface UseLiveStreamReturn<A, E> {\n /**\n * The latest emission as an {@link AsyncResult.AsyncResult}. Each emission\n * replaces the previous value (never appends). While re-subscribing after a\n * reactive-input change the previous value stays visible as a waiting\n * `Success` (stale-while-revalidate). Held in `$state.raw`, so a large array's\n * reference identity is preserved for downstream dedup.\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * A reactive `Promise` reflecting `current`, for `{#await}`-free async\n * templates (`experimental.async` + `<svelte:boundary>`). Pending until the\n * first emission, then replaced with an already-resolved promise on every\n * subsequent emission (no pending flash after first load); failures reject it.\n * With `initial` it starts already-resolved, so seeded pages never suspend\n * during SSR.\n */\n readonly promise: Promise<A>;\n}\n\n/**\n * A rune-backed {@link LiveResultState}. Holds the result in `$state.raw`:\n * emissions can be large arrays whose reference equality is meaningful\n * downstream, so deep proxying would break dedup and waste work.\n */\nfunction makeLiveResult<A, E>(seed: Option.Option<A>): LiveResultState<A, E> {\n let current = $state.raw<AsyncResult.AsyncResult<A, E>>(\n Option.isSome(seed) ? AsyncResult.success<A, E>(seed.value) : AsyncResult.initial<A, E>(),\n );\n\n return {\n get current() {\n return current;\n },\n startWaiting() {\n current = AsyncResult.waiting(current);\n },\n emit(value) {\n current = AsyncResult.success<A, E>(value);\n },\n failCause(cause) {\n current = AsyncResult.failureWithPrevious<A, E>(cause, { previous: Option.some(current) });\n },\n };\n}\n\n/**\n * Subscribe to a live `Stream` and track its latest emission, re-subscribing\n * whenever the reactive inputs it reads change.\n *\n * The `stream` thunk is called inside `$effect`, so any reactive state it reads\n * (e.g. a pagination cursor in `$state`) is dependency-tracked; when it changes,\n * the previous subscription is interrupted and a new one starts, keeping the\n * previous value visible as a waiting `Success` (stale-while-revalidate). This\n * is distinct from {@link useStream} (accumulates a static stream) and\n * `reactiveStream` (re-runs an *Effect* on Reactivity-key invalidation).\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useLiveStream } from '@thomasfosterau/effect-svelte';\n *\n * let cursor = $state(0);\n *\n * // Re-subscribes whenever `cursor` changes.\n * const todos = useLiveStream(() => liveTodos(cursor), { initial: seededTodos });\n * </script>\n *\n * <svelte:boundary>\n * <ul>{#each await todos.promise as todo}<li>{todo.title}</li>{/each}</ul>\n * </svelte:boundary>\n * ```\n */\nexport function useLiveStream<A, E, R>(\n stream: () => Stream.Stream<A, E, R>,\n options: UseLiveStreamOptions<A, R> = {},\n): UseLiveStreamReturn<A, E> {\n const runtime = options.runtime ?? (getRuntime() as RuntimeLike<R>);\n const seed: Option.Option<A> =\n \"initial\" in options ? Option.some(options.initial as A) : Option.none();\n\n const state = makeLiveResult<A, E>(seed);\n const core = makeLiveStream(runtime, state);\n\n $effect(() => {\n // Calling the thunk inside `$effect` tracks the reactive inputs it reads;\n // when they change, the effect re-runs — tearing down the old subscription\n // (interrupt, no Failure) and starting a fresh one.\n const current = stream();\n return core.subscribe(current);\n });\n\n // A new promise identity is produced only when the underlying result\n // transitions, so awaited template expressions re-run with fresh data instead\n // of on every unrelated re-render.\n const promise = $derived(resultToPromise(state.current));\n\n return {\n get current() {\n return state.current;\n },\n get promise() {\n return promise;\n },\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"live-stream.svelte.js","names":["getRuntime"],"sources":["../src/live-stream.svelte.ts"],"sourcesContent":["import { Option, type Stream } from \"effect\";\nimport * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { untrack } from \"svelte\";\nimport { getRuntime } from \"./context.svelte.js\";\nimport { resultToPromise } from \"./internal/await.js\";\nimport { makeLiveStream, type LiveResultState } from \"./internal/live-stream.js\";\nimport type { RuntimeLike } from \"./internal/run.js\";\n\nexport interface UseLiveStreamOptions<A, R> {\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * stream's `R` is constrained to what the runtime provides (a service-typed\n * `ManagedRuntime` gives a fully-checked `R`).\n */\n readonly runtime?: RuntimeLike<R>;\n\n /**\n * A synchronous seed value. When provided, `current` starts as a non-waiting\n * `Success(initial)` (and `promise` starts already-resolved) instead of\n * `Initial`, so server renders and the first client render show the data the\n * server already had — the first real emission then replaces it. This is the\n * SSR seam: without a synchronous seed, SSR output and the first client render\n * would show empty state and hydration would mismatch.\n */\n readonly initial?: A;\n}\n\nexport interface UseLiveStreamReturn<A, E> {\n /**\n * The latest emission as an {@link AsyncResult.AsyncResult}. Each emission\n * replaces the previous value (never appends). While re-subscribing after a\n * reactive-input change the previous value stays visible as a waiting\n * `Success` (stale-while-revalidate). Held in `$state.raw`, so a large array's\n * reference identity is preserved for downstream dedup.\n */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * A reactive `Promise` reflecting `current`, for `{#await}`-free async\n * templates (`experimental.async` + `<svelte:boundary>`). Pending until the\n * first emission, then replaced with an already-resolved promise on every\n * subsequent emission (no pending flash after first load); failures reject it.\n * With `initial` it starts already-resolved, so seeded pages never suspend\n * during SSR.\n */\n readonly promise: Promise<A>;\n}\n\n/**\n * A rune-backed {@link LiveResultState}. Holds the result in `$state.raw`:\n * emissions can be large arrays whose reference equality is meaningful\n * downstream, so deep proxying would break dedup and waste work.\n */\nfunction makeLiveResult<A, E>(seed: Option.Option<A>): LiveResultState<A, E> {\n let current = $state.raw<AsyncResult.AsyncResult<A, E>>(\n Option.isSome(seed) ? AsyncResult.success<A, E>(seed.value) : AsyncResult.initial<A, E>(),\n );\n\n return {\n get current() {\n return current;\n },\n startWaiting() {\n // `startWaiting` runs from inside the hook's `$effect` (via\n // `core.subscribe`), so reading `current` here must be untracked —\n // otherwise the effect depends on its own result state and every\n // emission (which writes `current`) re-triggers the effect, tearing\n // down and re-starting the subscription on every value.\n current = AsyncResult.waiting(untrack(() => current));\n },\n emit(value) {\n current = AsyncResult.success<A, E>(value);\n },\n failCause(cause) {\n current = AsyncResult.failureWithPrevious<A, E>(cause, { previous: Option.some(current) });\n },\n };\n}\n\n/**\n * Subscribe to a live `Stream` and track its latest emission, re-subscribing\n * whenever the reactive inputs it reads change.\n *\n * The `stream` thunk is called inside `$effect`, so any reactive state it reads\n * (e.g. a pagination cursor in `$state`) is dependency-tracked; when it changes,\n * the previous subscription is interrupted and a new one starts, keeping the\n * previous value visible as a waiting `Success` (stale-while-revalidate). This\n * is distinct from {@link useStream} (accumulates a static stream) and\n * `reactiveStream` (re-runs an *Effect* on Reactivity-key invalidation).\n *\n * @example\n * ```svelte\n * <script lang=\"ts\">\n * import { useLiveStream } from '@thomasfosterau/effect-svelte';\n *\n * let cursor = $state(0);\n *\n * // Re-subscribes whenever `cursor` changes.\n * const todos = useLiveStream(() => liveTodos(cursor), { initial: seededTodos });\n * </script>\n *\n * <svelte:boundary>\n * <ul>{#each await todos.promise as todo}<li>{todo.title}</li>{/each}</ul>\n * </svelte:boundary>\n * ```\n */\nexport function useLiveStream<A, E, R>(\n stream: () => Stream.Stream<A, E, R>,\n options: UseLiveStreamOptions<A, R> = {},\n): UseLiveStreamReturn<A, E> {\n const runtime = options.runtime ?? (getRuntime() as RuntimeLike<R>);\n const seed: Option.Option<A> =\n \"initial\" in options ? Option.some(options.initial as A) : Option.none();\n\n const state = makeLiveResult<A, E>(seed);\n const core = makeLiveStream(runtime, state);\n\n $effect(() => {\n // Calling the thunk inside `$effect` tracks the reactive inputs it reads;\n // when they change, the effect re-runs — tearing down the old subscription\n // (interrupt, no Failure) and starting a fresh one.\n const current = stream();\n return core.subscribe(current);\n });\n\n // A new promise identity is produced only when the underlying result\n // transitions, so awaited template expressions re-run with fresh data instead\n // of on every unrelated re-render.\n const promise = $derived(resultToPromise(state.current));\n\n return {\n get current() {\n return state.current;\n },\n get promise() {\n return promise;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;AAqDA,SAAS,eAAqB,MAA+C;CAC3E,IAAI,UAAU,OAAO,IACnB,OAAO,OAAO,IAAI,IAAI,YAAY,QAAc,KAAK,KAAK,IAAI,YAAY,QAAc,CAC1F;CAEA,OAAO;EACL,IAAI,UAAU;GACZ,OAAO;EACT;EACA,eAAe;GAMb,UAAU,YAAY,QAAQ,cAAc,OAAO,CAAC;EACtD;EACA,KAAK,OAAO;GACV,UAAU,YAAY,QAAc,KAAK;EAC3C;EACA,UAAU,OAAO;GACf,UAAU,YAAY,oBAA0B,OAAO,EAAE,UAAU,OAAO,KAAK,OAAO,EAAE,CAAC;EAC3F;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,cACd,QACA,UAAsC,CAAC,GACZ;CAC3B,MAAM,UAAU,QAAQ,WAAYA,kBAAW;CAI/C,MAAM,QAAQ,eAFZ,aAAa,UAAU,OAAO,KAAK,QAAQ,OAAY,IAAI,OAAO,KAAK,CAElC;CACvC,MAAM,OAAO,eAAe,SAAS,KAAK;CAE1C,cAAc;EAIZ,MAAM,UAAU,OAAO;EACvB,OAAO,KAAK,UAAU,OAAO;CAC/B,CAAC;CAKD,MAAM,UAAU,SAAS,gBAAgB,MAAM,OAAO,CAAC;CAEvD,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,MAAM;EACf;EACA,IAAI,UAAU;GACZ,OAAO;EACT;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomasfosterau/effect-svelte",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Effect integration for Svelte 5 runes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
34
34
|
"types": "./dist/index.d.ts",
|
|
35
|
+
"svelte": "./dist/index.js",
|
|
35
36
|
"import": "./dist/index.js"
|
|
36
37
|
}
|
|
37
38
|
},
|