@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.
- package/dist/await.svelte.d.ts +53 -0
- package/dist/await.svelte.js +44 -0
- package/dist/await.svelte.js.map +1 -0
- package/dist/context.svelte.d.ts +51 -1
- package/dist/context.svelte.js +43 -1
- package/dist/context.svelte.js.map +1 -1
- package/dist/derived.svelte.d.ts +32 -12
- package/dist/derived.svelte.js +23 -13
- package/dist/derived.svelte.js.map +1 -1
- package/dist/effect.svelte.d.ts +21 -2
- package/dist/effect.svelte.js +13 -3
- package/dist/effect.svelte.js.map +1 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.js +6 -2
- package/dist/internal/await.js +52 -0
- package/dist/internal/await.js.map +1 -0
- package/dist/internal/live-stream.js +43 -0
- package/dist/internal/live-stream.js.map +1 -0
- package/dist/internal/mutation.d.ts +38 -0
- package/dist/internal/mutation.js +61 -0
- package/dist/internal/mutation.js.map +1 -0
- package/dist/internal/result.svelte.d.ts +1 -0
- package/dist/internal/writable.js +21 -0
- package/dist/internal/writable.js.map +1 -0
- package/dist/live-stream.svelte.d.ts +72 -0
- package/dist/live-stream.svelte.js +77 -0
- package/dist/live-stream.svelte.js.map +1 -0
- package/dist/mutation.svelte.d.ts +65 -0
- package/dist/mutation.svelte.js +53 -0
- package/dist/mutation.svelte.js.map +1 -0
- package/dist/query.svelte.d.ts +21 -2
- package/dist/query.svelte.js +10 -3
- package/dist/query.svelte.js.map +1 -1
- package/dist/reactivity.svelte.d.ts +15 -5
- package/dist/reactivity.svelte.js +8 -8
- package/dist/reactivity.svelte.js.map +1 -1
- package/dist/stream.svelte.d.ts +10 -2
- package/dist/stream.svelte.js +2 -2
- package/dist/stream.svelte.js.map +1 -1
- package/dist/subscription.svelte.d.ts +12 -3
- package/dist/subscription.svelte.js +4 -4
- package/dist/subscription.svelte.js.map +1 -1
- package/dist/writable-ref.svelte.d.ts +53 -0
- package/dist/writable-ref.svelte.js +66 -0
- package/dist/writable-ref.svelte.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { interruptFiber, runFork } from "./run.js";
|
|
2
|
+
import { Cause, Effect, Exit, Stream } from "effect";
|
|
3
|
+
//#region src/internal/live-stream.ts
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrates a live `Stream` subscription against a {@link LiveResultState}.
|
|
6
|
+
*
|
|
7
|
+
* A single {@link makeLiveStream} instance is reused across re-subscriptions
|
|
8
|
+
* (driven by the hook's `$effect`): every call to `subscribe` interrupts the
|
|
9
|
+
* previous run and starts a fresh one, keeping the previous value visible as a
|
|
10
|
+
* waiting `Success` in between (stale-while-revalidate).
|
|
11
|
+
*
|
|
12
|
+
* Interruption caused by the hook's own teardown / re-subscribe must never
|
|
13
|
+
* surface as a `Failure` — only genuine failures (`Cause.hasFails`) and defects
|
|
14
|
+
* (`Cause.hasDies`) settle the result to `Failure`.
|
|
15
|
+
*/
|
|
16
|
+
function makeLiveStream(runtime, state) {
|
|
17
|
+
let active = null;
|
|
18
|
+
const subscribe = (stream) => {
|
|
19
|
+
const token = Symbol();
|
|
20
|
+
active = token;
|
|
21
|
+
state.startWaiting();
|
|
22
|
+
const consume = Stream.runForEach(stream, (value) => Effect.sync(() => {
|
|
23
|
+
if (active !== token) return;
|
|
24
|
+
state.emit(value);
|
|
25
|
+
}));
|
|
26
|
+
const program = Effect.flatMap(Effect.yieldNow, () => consume).pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
27
|
+
if (active !== token) return;
|
|
28
|
+
if (Exit.isSuccess(exit)) return;
|
|
29
|
+
const cause = exit.cause;
|
|
30
|
+
if (Cause.hasFails(cause) || Cause.hasDies(cause)) state.failCause(cause);
|
|
31
|
+
})));
|
|
32
|
+
const fiber = runFork(runtime)(program);
|
|
33
|
+
return () => {
|
|
34
|
+
if (active === token) active = null;
|
|
35
|
+
interruptFiber(fiber);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
return { subscribe };
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { makeLiveStream };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=live-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-stream.js","names":[],"sources":["../../src/internal/live-stream.ts"],"sourcesContent":["import { Cause, Effect, Exit, Stream } from \"effect\";\nimport type * as AsyncResult from \"effect/unstable/reactivity/AsyncResult\";\nimport { interruptFiber, runFork, type RuntimeLike } from \"./run.js\";\n\n/**\n * The reactive holder driven by {@link makeLiveStream}. The rune-backed\n * implementation lives in `live-stream.svelte.ts` (a `$state.raw` cell); this\n * interface keeps the orchestrator below rune-free so it can be unit-tested\n * with a plain fake (the same split as `makeResult` / `makeMutation`).\n *\n * The transitions follow the same stale-while-revalidate semantics as the rest\n * of the package: marking a result waiting preserves whatever value it already\n * holds, so the previous emission stays visible while a new subscription spins\n * up.\n */\nexport interface LiveResultState<A, E> {\n /** The current result. Read inside an effect or template to track it. */\n readonly current: AsyncResult.AsyncResult<A, E>;\n\n /**\n * Mark the current result as waiting, preserving any value it already holds\n * (a seeded `Success` becomes a waiting `Success`, an `Initial` a waiting\n * `Initial`).\n */\n startWaiting(): void;\n\n /**\n * Replace the result with a non-waiting `Success`. Each emission replaces the\n * previous one — live queries emit whole result sets, never appended deltas.\n */\n emit(value: A): void;\n\n /** Replace the result with a `Failure`, carrying the previous success forward. */\n failCause(cause: Cause.Cause<E>): void;\n}\n\n/**\n * Orchestrates a live `Stream` subscription against a {@link LiveResultState}.\n *\n * A single {@link makeLiveStream} instance is reused across re-subscriptions\n * (driven by the hook's `$effect`): every call to `subscribe` interrupts the\n * previous run and starts a fresh one, keeping the previous value visible as a\n * waiting `Success` in between (stale-while-revalidate).\n *\n * Interruption caused by the hook's own teardown / re-subscribe must never\n * surface as a `Failure` — only genuine failures (`Cause.hasFails`) and defects\n * (`Cause.hasDies`) settle the result to `Failure`.\n */\nexport function makeLiveStream<A, E, R>(\n runtime: RuntimeLike<R>,\n state: LiveResultState<A, E>,\n): {\n /** Start a new subscription, returning a teardown that interrupts it. */\n readonly subscribe: (stream: Stream.Stream<A, E, R>) => () => void;\n} {\n // Identifies the live subscription. A superseded run's late emission (or\n // exit) is dropped by comparing against this token, so a fiber that is being\n // interrupted cannot clobber the value the newer subscription just set.\n let active: symbol | null = null;\n\n const subscribe = (stream: Stream.Stream<A, E, R>): (() => void) => {\n const token = Symbol();\n active = token;\n\n // Keep the previous value visible as a waiting `Success` while the new\n // subscription starts (stale-while-revalidate). On the very first run this\n // marks a seeded `Success` / bare `Initial` as waiting.\n state.startWaiting();\n\n const consume = Stream.runForEach(stream, (value) =>\n Effect.sync(() => {\n if (active !== token) return;\n // Each emission replaces the current value — never appends.\n state.emit(value);\n }),\n );\n\n // Yield once before consuming, so a synchronously-resolving stream cannot\n // write state *during* the `$effect` flush that started it (Svelte flags a\n // write-during-flush that the awaited `promise` reads back as an update\n // cycle). The subscription still starts on the very next microtask.\n const program = Effect.flatMap(Effect.yieldNow, () => consume).pipe(\n Effect.onExit((exit) =>\n Effect.sync(() => {\n if (active !== token) return;\n // A completed stream leaves the last emission in place.\n if (Exit.isSuccess(exit)) return;\n const cause = exit.cause;\n // Teardown / re-subscribe interruption is normal lifecycle churn and\n // must not surface as a `Failure`; only real failures and defects do.\n if (Cause.hasFails(cause) || Cause.hasDies(cause)) {\n state.failCause(cause);\n }\n }),\n ),\n );\n\n const fiber = runFork(runtime)(program);\n\n return () => {\n if (active === token) active = null;\n interruptFiber(fiber);\n };\n };\n\n return { subscribe };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgDA,SAAgB,eACd,SACA,OAIA;CAIA,IAAI,SAAwB;CAE5B,MAAM,aAAa,WAAiD;EAClE,MAAM,QAAQ,OAAO;EACrB,SAAS;EAKT,MAAM,aAAa;EAEnB,MAAM,UAAU,OAAO,WAAW,SAAS,UACzC,OAAO,WAAW;GAChB,IAAI,WAAW,OAAO;GAEtB,MAAM,KAAK,KAAK;EAClB,CAAC,CACH;EAMA,MAAM,UAAU,OAAO,QAAQ,OAAO,gBAAgB,OAAO,EAAE,KAC7D,OAAO,QAAQ,SACb,OAAO,WAAW;GAChB,IAAI,WAAW,OAAO;GAEtB,IAAI,KAAK,UAAU,IAAI,GAAG;GAC1B,MAAM,QAAQ,KAAK;GAGnB,IAAI,MAAM,SAAS,KAAK,KAAK,MAAM,QAAQ,KAAK,GAC9C,MAAM,UAAU,KAAK;EAEzB,CAAC,CACH,CACF;EAEA,MAAM,QAAQ,QAAQ,OAAO,EAAE,OAAO;EAEtC,aAAa;GACX,IAAI,WAAW,OAAO,SAAS;GAC/B,eAAe,KAAK;EACtB;CACF;CAEA,OAAO,EAAE,UAAU;AACrB"}
|
|
@@ -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, R = never>(\n runtime: RuntimeLike<R>,\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,72 @@
|
|
|
1
|
+
import { RuntimeLike } from "./internal/run.js";
|
|
2
|
+
import { Stream } from "effect";
|
|
3
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
4
|
+
|
|
5
|
+
//#region src/live-stream.svelte.d.ts
|
|
6
|
+
interface UseLiveStreamOptions<A, R> {
|
|
7
|
+
/**
|
|
8
|
+
* Run against this runtime instead of the ambient context. When provided, the
|
|
9
|
+
* stream's `R` is constrained to what the runtime provides (a service-typed
|
|
10
|
+
* `ManagedRuntime` gives a fully-checked `R`).
|
|
11
|
+
*/
|
|
12
|
+
readonly runtime?: RuntimeLike<R>;
|
|
13
|
+
/**
|
|
14
|
+
* A synchronous seed value. When provided, `current` starts as a non-waiting
|
|
15
|
+
* `Success(initial)` (and `promise` starts already-resolved) instead of
|
|
16
|
+
* `Initial`, so server renders and the first client render show the data the
|
|
17
|
+
* server already had — the first real emission then replaces it. This is the
|
|
18
|
+
* SSR seam: without a synchronous seed, SSR output and the first client render
|
|
19
|
+
* would show empty state and hydration would mismatch.
|
|
20
|
+
*/
|
|
21
|
+
readonly initial?: A;
|
|
22
|
+
}
|
|
23
|
+
interface UseLiveStreamReturn<A, E> {
|
|
24
|
+
/**
|
|
25
|
+
* The latest emission as an {@link AsyncResult.AsyncResult}. Each emission
|
|
26
|
+
* replaces the previous value (never appends). While re-subscribing after a
|
|
27
|
+
* reactive-input change the previous value stays visible as a waiting
|
|
28
|
+
* `Success` (stale-while-revalidate). Held in `$state.raw`, so a large array's
|
|
29
|
+
* reference identity is preserved for downstream dedup.
|
|
30
|
+
*/
|
|
31
|
+
readonly current: AsyncResult.AsyncResult<A, E>;
|
|
32
|
+
/**
|
|
33
|
+
* A reactive `Promise` reflecting `current`, for `{#await}`-free async
|
|
34
|
+
* templates (`experimental.async` + `<svelte:boundary>`). Pending until the
|
|
35
|
+
* first emission, then replaced with an already-resolved promise on every
|
|
36
|
+
* subsequent emission (no pending flash after first load); failures reject it.
|
|
37
|
+
* With `initial` it starts already-resolved, so seeded pages never suspend
|
|
38
|
+
* during SSR.
|
|
39
|
+
*/
|
|
40
|
+
readonly promise: Promise<A>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to a live `Stream` and track its latest emission, re-subscribing
|
|
44
|
+
* whenever the reactive inputs it reads change.
|
|
45
|
+
*
|
|
46
|
+
* The `stream` thunk is called inside `$effect`, so any reactive state it reads
|
|
47
|
+
* (e.g. a pagination cursor in `$state`) is dependency-tracked; when it changes,
|
|
48
|
+
* the previous subscription is interrupted and a new one starts, keeping the
|
|
49
|
+
* previous value visible as a waiting `Success` (stale-while-revalidate). This
|
|
50
|
+
* is distinct from {@link useStream} (accumulates a static stream) and
|
|
51
|
+
* `reactiveStream` (re-runs an *Effect* on Reactivity-key invalidation).
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```svelte
|
|
55
|
+
* <script lang="ts">
|
|
56
|
+
* import { useLiveStream } from '@thomasfosterau/effect-svelte';
|
|
57
|
+
*
|
|
58
|
+
* let cursor = $state(0);
|
|
59
|
+
*
|
|
60
|
+
* // Re-subscribes whenever `cursor` changes.
|
|
61
|
+
* const todos = useLiveStream(() => liveTodos(cursor), { initial: seededTodos });
|
|
62
|
+
* </script>
|
|
63
|
+
*
|
|
64
|
+
* <svelte:boundary>
|
|
65
|
+
* <ul>{#each await todos.promise as todo}<li>{todo.title}</li>{/each}</ul>
|
|
66
|
+
* </svelte:boundary>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare function useLiveStream<A, E, R>(stream: () => Stream.Stream<A, E, R>, options?: UseLiveStreamOptions<A, R>): UseLiveStreamReturn<A, E>;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { UseLiveStreamOptions, UseLiveStreamReturn, useLiveStream };
|
|
72
|
+
//# sourceMappingURL=live-stream.svelte.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getRuntimeContext } from "./context.svelte.js";
|
|
2
|
+
import { resultToPromise } from "./internal/await.js";
|
|
3
|
+
import { makeLiveStream } from "./internal/live-stream.js";
|
|
4
|
+
import { Option } from "effect";
|
|
5
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
6
|
+
//#region src/live-stream.svelte.ts
|
|
7
|
+
/**
|
|
8
|
+
* A rune-backed {@link LiveResultState}. Holds the result in `$state.raw`:
|
|
9
|
+
* emissions can be large arrays whose reference equality is meaningful
|
|
10
|
+
* downstream, so deep proxying would break dedup and waste work.
|
|
11
|
+
*/
|
|
12
|
+
function makeLiveResult(seed) {
|
|
13
|
+
let current = $state.raw(Option.isSome(seed) ? AsyncResult.success(seed.value) : AsyncResult.initial());
|
|
14
|
+
return {
|
|
15
|
+
get current() {
|
|
16
|
+
return current;
|
|
17
|
+
},
|
|
18
|
+
startWaiting() {
|
|
19
|
+
current = AsyncResult.waiting(current);
|
|
20
|
+
},
|
|
21
|
+
emit(value) {
|
|
22
|
+
current = AsyncResult.success(value);
|
|
23
|
+
},
|
|
24
|
+
failCause(cause) {
|
|
25
|
+
current = AsyncResult.failureWithPrevious(cause, { previous: Option.some(current) });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Subscribe to a live `Stream` and track its latest emission, re-subscribing
|
|
31
|
+
* whenever the reactive inputs it reads change.
|
|
32
|
+
*
|
|
33
|
+
* The `stream` thunk is called inside `$effect`, so any reactive state it reads
|
|
34
|
+
* (e.g. a pagination cursor in `$state`) is dependency-tracked; when it changes,
|
|
35
|
+
* the previous subscription is interrupted and a new one starts, keeping the
|
|
36
|
+
* previous value visible as a waiting `Success` (stale-while-revalidate). This
|
|
37
|
+
* is distinct from {@link useStream} (accumulates a static stream) and
|
|
38
|
+
* `reactiveStream` (re-runs an *Effect* on Reactivity-key invalidation).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```svelte
|
|
42
|
+
* <script lang="ts">
|
|
43
|
+
* import { useLiveStream } from '@thomasfosterau/effect-svelte';
|
|
44
|
+
*
|
|
45
|
+
* let cursor = $state(0);
|
|
46
|
+
*
|
|
47
|
+
* // Re-subscribes whenever `cursor` changes.
|
|
48
|
+
* const todos = useLiveStream(() => liveTodos(cursor), { initial: seededTodos });
|
|
49
|
+
* <\/script>
|
|
50
|
+
*
|
|
51
|
+
* <svelte:boundary>
|
|
52
|
+
* <ul>{#each await todos.promise as todo}<li>{todo.title}</li>{/each}</ul>
|
|
53
|
+
* </svelte:boundary>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function useLiveStream(stream, options = {}) {
|
|
57
|
+
const runtime = options.runtime ?? getRuntimeContext();
|
|
58
|
+
const state = makeLiveResult("initial" in options ? Option.some(options.initial) : Option.none());
|
|
59
|
+
const core = makeLiveStream(runtime, state);
|
|
60
|
+
$effect(() => {
|
|
61
|
+
const current = stream();
|
|
62
|
+
return core.subscribe(current);
|
|
63
|
+
});
|
|
64
|
+
const promise = $derived(resultToPromise(state.current));
|
|
65
|
+
return {
|
|
66
|
+
get current() {
|
|
67
|
+
return state.current;
|
|
68
|
+
},
|
|
69
|
+
get promise() {
|
|
70
|
+
return promise;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { useLiveStream };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=live-stream.svelte.js.map
|
|
@@ -0,0 +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":";;;;;;;;;;;AAoDA,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;GACb,UAAU,YAAY,QAAQ,OAAO;EACvC;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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RuntimeLike } from "./internal/run.js";
|
|
2
|
+
import { MutationCallbacks } from "./internal/mutation.js";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
5
|
+
|
|
6
|
+
//#region src/mutation.svelte.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Options for {@link useMutation}: the lifecycle {@link MutationCallbacks} plus
|
|
9
|
+
* an optional explicit `runtime`.
|
|
10
|
+
*/
|
|
11
|
+
type UseMutationOptions<A, E, I, R, C> = MutationCallbacks<A, E, I, C> & {
|
|
12
|
+
/**
|
|
13
|
+
* Run against this runtime instead of the ambient context. When provided, the
|
|
14
|
+
* mutation's `R` is constrained to what the runtime provides.
|
|
15
|
+
*/
|
|
16
|
+
readonly runtime?: RuntimeLike<R>;
|
|
17
|
+
};
|
|
18
|
+
interface UseMutationReturn<A, E, I> {
|
|
19
|
+
/**
|
|
20
|
+
* The current result state of the mutation. Starts `Initial`; each `mutate`
|
|
21
|
+
* marks it waiting (preserving the previous value) and settles to
|
|
22
|
+
* `Success` / `Failure`.
|
|
23
|
+
*/
|
|
24
|
+
readonly current: AsyncResult.AsyncResult<A, E>;
|
|
25
|
+
/** Run the mutation with `input`. A new call interrupts any in-flight run. */
|
|
26
|
+
readonly mutate: (input: I) => void;
|
|
27
|
+
/** Interrupt any in-flight run and reset `current` back to `Initial`. */
|
|
28
|
+
readonly reset: () => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A TanStack-Query-shaped mutation hook: run an effectful `fn(input)` on
|
|
32
|
+
* demand, track its {@link AsyncResult.AsyncResult}, and fire lifecycle
|
|
33
|
+
* callbacks.
|
|
34
|
+
*
|
|
35
|
+
* Optimistic updates follow the TanStack model — apply the change in `onMutate`
|
|
36
|
+
* and return a snapshot as `context`, then roll back in `onError`. `invalidates`
|
|
37
|
+
* re-runs any reactive queries depending on the given keys after success (this
|
|
38
|
+
* requires the runtime to include `Reactivity.layer`).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```svelte
|
|
42
|
+
* <script lang="ts">
|
|
43
|
+
* import { useMutation, AsyncResult } from '@thomasfosterau/effect-svelte';
|
|
44
|
+
* import { Effect } from 'effect';
|
|
45
|
+
*
|
|
46
|
+
* const addTodo = useMutation(
|
|
47
|
+
* (title: string) => saveTodo(title),
|
|
48
|
+
* {
|
|
49
|
+
* invalidates: ['todos'],
|
|
50
|
+
* onSuccess: (todo) => console.log('added', todo.id),
|
|
51
|
+
* },
|
|
52
|
+
* );
|
|
53
|
+
* </script>
|
|
54
|
+
*
|
|
55
|
+
* <button onclick={() => addTodo.mutate('Buy milk')}>Add</button>
|
|
56
|
+
*
|
|
57
|
+
* {#if AsyncResult.isWaiting(addTodo.current)}
|
|
58
|
+
* <p>Saving…</p>
|
|
59
|
+
* {/if}
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
declare function useMutation<A, E, I, R, C = unknown>(fn: (input: I) => Effect.Effect<A, E, R>, options?: UseMutationOptions<A, E, I, R, C>): UseMutationReturn<A, E, I>;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { UseMutationOptions, UseMutationReturn, useMutation };
|
|
65
|
+
//# 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 = options.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\n/**\n * Options for {@link useMutation}: the lifecycle {@link MutationCallbacks} plus\n * an optional explicit `runtime`.\n */\nexport type UseMutationOptions<A, E, I, R, C> = MutationCallbacks<A, E, I, C> & {\n /**\n * Run against this runtime instead of the ambient context. When provided, the\n * mutation's `R` is constrained to what the runtime provides.\n */\n readonly runtime?: RuntimeLike<R>;\n};\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: UseMutationOptions<A, E, I, R, C> = {},\n): UseMutationReturn<A, E, I> {\n const runtime = options.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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,SAAgB,YACd,IACA,UAA6C,CAAC,GAClB;CAC5B,MAAM,UAAU,QAAQ,WAAYA,kBAAW;CAC/C,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"}
|
package/dist/query.svelte.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
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/query.svelte.d.ts
|
|
6
|
+
interface UseQueryOptions<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
|
+
}
|
|
5
13
|
interface UseQueryReturn<A, E> {
|
|
6
14
|
/**
|
|
7
15
|
* The current result state of the query. Refetching preserves the previous
|
|
@@ -12,6 +20,17 @@ interface UseQueryReturn<A, E> {
|
|
|
12
20
|
* Re-fetch the data by re-running the effect
|
|
13
21
|
*/
|
|
14
22
|
refetch: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Optimistically overwrite the query's data locally without refetching.
|
|
25
|
+
* Accepts a value or an updater over the current value (`undefined` if none
|
|
26
|
+
* yet). The next `refetch` reconciles it with the server.
|
|
27
|
+
*/
|
|
28
|
+
setData: (updater: A | ((previous: A | undefined) => A)) => void;
|
|
29
|
+
/**
|
|
30
|
+
* `true` while showing a previous value during a refetch
|
|
31
|
+
* (stale-while-revalidate).
|
|
32
|
+
*/
|
|
33
|
+
readonly isStale: boolean;
|
|
15
34
|
}
|
|
16
35
|
/**
|
|
17
36
|
* Query pattern with refetch capability.
|
|
@@ -39,7 +58,7 @@ interface UseQueryReturn<A, E> {
|
|
|
39
58
|
* {/if}
|
|
40
59
|
* ```
|
|
41
60
|
*/
|
|
42
|
-
declare function useQuery<A, E, R>(effect: Effect.Effect<A, E, R>): UseQueryReturn<A, E>;
|
|
61
|
+
declare function useQuery<A, E, R>(effect: Effect.Effect<A, E, R>, options?: UseQueryOptions<R>): UseQueryReturn<A, E>;
|
|
43
62
|
//#endregion
|
|
44
|
-
export { UseQueryReturn, useQuery };
|
|
63
|
+
export { UseQueryOptions, UseQueryReturn, useQuery };
|
|
45
64
|
//# sourceMappingURL=query.svelte.d.ts.map
|
package/dist/query.svelte.js
CHANGED
|
@@ -26,13 +26,20 @@ import { useEffect } from "./effect.svelte.js";
|
|
|
26
26
|
* {/if}
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
function useQuery(effect) {
|
|
30
|
-
const effectHook = useEffect(effect, {
|
|
29
|
+
function useQuery(effect, options = {}) {
|
|
30
|
+
const effectHook = useEffect(effect, {
|
|
31
|
+
immediate: true,
|
|
32
|
+
...options
|
|
33
|
+
});
|
|
31
34
|
return {
|
|
32
35
|
get current() {
|
|
33
36
|
return effectHook.current;
|
|
34
37
|
},
|
|
35
|
-
refetch: effectHook.run
|
|
38
|
+
refetch: effectHook.run,
|
|
39
|
+
setData: effectHook.setData,
|
|
40
|
+
get isStale() {
|
|
41
|
+
return effectHook.isStale;
|
|
42
|
+
}
|
|
36
43
|
};
|
|
37
44
|
}
|
|
38
45
|
//#endregion
|
package/dist/query.svelte.js.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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\";\nimport type { RuntimeLike } from \"./internal/run.js\";\n\nexport interface UseQueryOptions<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 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>(\n effect: Effect.Effect<A, E, R>,\n options: UseQueryOptions<R> = {},\n): UseQueryReturn<A, E> {\n const effectHook = useEffect(effect, { immediate: true, ...options });\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,SACd,QACA,UAA8B,CAAC,GACT;CACtB,MAAM,aAAa,UAAU,QAAQ;EAAE,WAAW;EAAM,GAAG;CAAQ,CAAC;CAEpE,OAAO;EACL,IAAI,UAAU;GACZ,OAAO,WAAW;EACpB;EACA,SAAS,WAAW;EACpB,SAAS,WAAW;EACpB,IAAI,UAAU;GACZ,OAAO,WAAW;EACpB;CACF;AACF"}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import { RuntimeLike } from "./internal/run.js";
|
|
1
2
|
import { Effect } from "effect";
|
|
2
3
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
|
|
4
|
+
import { Reactivity } from "effect/unstable/reactivity";
|
|
3
5
|
|
|
4
6
|
//#region src/reactivity.svelte.d.ts
|
|
5
7
|
interface ReactiveQueryReturn<A, E> {
|
|
6
8
|
/** The current result, re-derived whenever the keys are invalidated. */
|
|
7
9
|
readonly current: AsyncResult.AsyncResult<A, E>;
|
|
8
10
|
}
|
|
11
|
+
interface ReactiveOptions<R> {
|
|
12
|
+
/**
|
|
13
|
+
* Run against this runtime instead of the ambient context. Must still provide
|
|
14
|
+
* `Reactivity.Reactivity`; when provided, the effect's `R` is otherwise
|
|
15
|
+
* constrained to what the runtime provides.
|
|
16
|
+
*/
|
|
17
|
+
readonly runtime?: RuntimeLike<R | Reactivity.Reactivity>;
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* Creates a reactive query that automatically re-runs when the specified keys are invalidated.
|
|
11
21
|
*
|
|
@@ -37,7 +47,7 @@ interface ReactiveQueryReturn<A, E> {
|
|
|
37
47
|
* {/if}
|
|
38
48
|
* ```
|
|
39
49
|
*/
|
|
40
|
-
declare function reactiveQuery<A, E, R>(keys: () => ReadonlyArray<unknown>, effectFn: () => Effect.Effect<A, E, R>): ReactiveQueryReturn<A, E>;
|
|
50
|
+
declare function reactiveQuery<A, E, R>(keys: () => ReadonlyArray<unknown>, effectFn: () => Effect.Effect<A, E, R>, options?: ReactiveOptions<R>): ReactiveQueryReturn<A, E>;
|
|
41
51
|
interface ReactiveStreamReturn<A, E> {
|
|
42
52
|
readonly current: AsyncResult.AsyncResult<ReadonlyArray<A>, E>;
|
|
43
53
|
readonly values: ReadonlyArray<A>;
|
|
@@ -72,7 +82,7 @@ interface ReactiveStreamReturn<A, E> {
|
|
|
72
82
|
* </ul>
|
|
73
83
|
* ```
|
|
74
84
|
*/
|
|
75
|
-
declare function reactiveStream<A, E, R>(keys: ReadonlyArray<unknown> | (() => ReadonlyArray<unknown>), effect: Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)): ReactiveStreamReturn<A, E>;
|
|
85
|
+
declare function reactiveStream<A, E, R>(keys: ReadonlyArray<unknown> | (() => ReadonlyArray<unknown>), effect: Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>), options?: ReactiveOptions<R>): ReactiveStreamReturn<A, E>;
|
|
76
86
|
interface ReactiveMutationReturn<A, E> {
|
|
77
87
|
readonly current: AsyncResult.AsyncResult<A, E>;
|
|
78
88
|
run: () => void;
|
|
@@ -101,7 +111,7 @@ interface ReactiveMutationReturn<A, E> {
|
|
|
101
111
|
* <button onclick={() => updateUser.run()}>Update</button>
|
|
102
112
|
* ```
|
|
103
113
|
*/
|
|
104
|
-
declare function reactiveMutation<A, E, R>(keys: ReadonlyArray<unknown> | (() => ReadonlyArray<unknown>), effect: Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>)): ReactiveMutationReturn<A, E>;
|
|
114
|
+
declare function reactiveMutation<A, E, R>(keys: ReadonlyArray<unknown> | (() => ReadonlyArray<unknown>), effect: Effect.Effect<A, E, R> | (() => Effect.Effect<A, E, R>), options?: ReactiveOptions<R>): ReactiveMutationReturn<A, E>;
|
|
105
115
|
/**
|
|
106
116
|
* Returns a function that manually invalidates reactivity keys, causing any
|
|
107
117
|
* reactive queries depending on those keys to re-run.
|
|
@@ -117,7 +127,7 @@ declare function reactiveMutation<A, E, R>(keys: ReadonlyArray<unknown> | (() =>
|
|
|
117
127
|
* <button onclick={() => invalidate(['users'])}>Refresh Users</button>
|
|
118
128
|
* ```
|
|
119
129
|
*/
|
|
120
|
-
declare function useInvalidateKeys(): (keys: ReadonlyArray<unknown>) => void;
|
|
130
|
+
declare function useInvalidateKeys(options?: ReactiveOptions<never>): (keys: ReadonlyArray<unknown>) => void;
|
|
121
131
|
//#endregion
|
|
122
|
-
export { ReactiveMutationReturn, ReactiveQueryReturn, ReactiveStreamReturn, reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys };
|
|
132
|
+
export { ReactiveMutationReturn, ReactiveOptions, ReactiveQueryReturn, ReactiveStreamReturn, reactiveMutation, reactiveQuery, reactiveStream, useInvalidateKeys };
|
|
123
133
|
//# sourceMappingURL=reactivity.svelte.d.ts.map
|