diff --git a/.changeset/fruity-years-drive.md b/.changeset/fruity-years-drive.md
new file mode 100644
index 00000000..d091117a
--- /dev/null
+++ b/.changeset/fruity-years-drive.md
@@ -0,0 +1,5 @@
+---
+"@effect-rx/rx": patch
+---
+
+allow intermediate updates in optimisticFn
diff --git a/docs/rx/Rx.ts.md b/docs/rx/Rx.ts.md
index 0c590c97..ed28ffca 100644
--- a/docs/rx/Rx.ts.md
+++ b/docs/rx/Rx.ts.md
@@ -40,6 +40,7 @@ Added in v1.0.0
- [batching](#batching)
- [batch](#batch)
- [combinators](#combinators)
+ - [autoDispose](#autodispose)
- [debounce](#debounce)
- [initialValue](#initialvalue)
- [keepAlive](#keepalive)
@@ -243,9 +244,7 @@ Added in v1.0.0
**Signature**
```ts
-export declare const optimistic: (
- self: Rx
-) => Writable ? _A : A, unknown>>>
+export declare const optimistic: (self: Rx) => Writable>>
```
Added in v1.0.0
@@ -256,15 +255,19 @@ Added in v1.0.0
```ts
export declare const optimisticFn: {
- ? _A : A>(options: {
- readonly updateToValue: (value: OW, current: NoInfer) => NoInfer
- readonly fn: RxResultFn, XA, XE>
+ (options: {
+ readonly reducer: (current: NoInfer, update: OW) => NoInfer
+ readonly fn:
+ | RxResultFn, XA, XE>
+ | ((set: (result: NoInfer) => void) => RxResultFn, XA, XE>)
}): (self: Writable>>) => RxResultFn
- ? _A : A>(
+ (
self: Writable>>,
options: {
- readonly updateToValue: (value: OW, current: NoInfer) => NoInfer
- readonly fn: RxResultFn, XA, XE>
+ readonly reducer: (current: NoInfer, update: OW) => NoInfer
+ readonly fn:
+ | RxResultFn, XA, XE>
+ | ((set: (result: NoInfer) => void) => RxResultFn, XA, XE>)
}
): RxResultFn
}
@@ -353,6 +356,21 @@ Added in v1.0.0
# combinators
+## autoDispose
+
+Reverts the `keepAlive` behavior of a reactive value, allowing it to be
+disposed of when not in use.
+
+Note that Rx's have this behavior by default.
+
+**Signature**
+
+```ts
+export declare const autoDispose: >(self: A) => A
+```
+
+Added in v1.0.0
+
## debounce
**Signature**
diff --git a/packages/rx/src/Rx.ts b/packages/rx/src/Rx.ts
index 7ce6770b..cf314a4f 100644
--- a/packages/rx/src/Rx.ts
+++ b/packages/rx/src/Rx.ts
@@ -1192,6 +1192,21 @@ export const keepAlive = >(self: A): A =>
keepAlive: true
})
+/**
+ * Reverts the `keepAlive` behavior of a reactive value, allowing it to be
+ * disposed of when not in use.
+ *
+ * Note that Rx's have this behavior by default.
+ *
+ * @since 1.0.0
+ * @category combinators
+ */
+export const autoDispose = >(self: A): A =>
+ Object.assign(Object.create(Object.getPrototypeOf(self)), {
+ ...self,
+ keepAlive: false
+ })
+
/**
* @since 1.0.0
* @category combinators
@@ -1433,7 +1448,9 @@ export const optimisticFn: {
(
options: {
readonly reducer: (current: NoInfer, update: OW) => NoInfer
- readonly fn: RxResultFn, XA, XE>
+ readonly fn:
+ | RxResultFn, XA, XE>
+ | ((set: (result: NoInfer) => void) => RxResultFn, XA, XE>)
}
): (
self: Writable>>
@@ -1442,14 +1459,18 @@ export const optimisticFn: {
self: Writable>>,
options: {
readonly reducer: (current: NoInfer, update: OW) => NoInfer
- readonly fn: RxResultFn, XA, XE>
+ readonly fn:
+ | RxResultFn, XA, XE>
+ | ((set: (result: NoInfer) => void) => RxResultFn, XA, XE>)
}
): RxResultFn
} = dual(2, (
self: Writable>>,
options: {
readonly reducer: (current: NoInfer, update: OW) => NoInfer
- readonly fn: RxResultFn
+ readonly fn:
+ | RxResultFn, XA, XE>
+ | ((set: (result: NoInfer) => void) => RxResultFn, XA, XE>)
}
): RxResultFn => {
const transition = state>(Result.initial())
@@ -1460,8 +1481,16 @@ export const optimisticFn: {
}
get.set(transition, Result.success(value, { waiting: true }))
get.set(self, transition)
- get.set(options.fn, arg)
- return Effect.onExit(get.result(options.fn, { suspendOnWaiting: true }), (exit) => {
+ const fn = typeof options.fn === "function"
+ ? autoDispose(options.fn((value) =>
+ get.set(
+ transition,
+ Result.success(Result.isResult(value) ? Result.waiting(value) : value, { waiting: true })
+ )
+ ))
+ : options.fn
+ get.set(fn, arg)
+ return Effect.onExit(get.result(fn, { suspendOnWaiting: true }), (exit) => {
get.set(transition, Result.fromExit(Exit.as(exit, value)))
return Effect.void
})
diff --git a/packages/rx/test/Rx.test.ts b/packages/rx/test/Rx.test.ts
index 0e5e095d..92264796 100644
--- a/packages/rx/test/Rx.test.ts
+++ b/packages/rx/test/Rx.test.ts
@@ -1171,6 +1171,44 @@ describe("Rx", () => {
expect(r.get(rx)).toEqual(2)
expect(r.get(optimisticRx)).toEqual(2)
})
+
+ it("intermediate updates", async () => {
+ const latch = Effect.unsafeMakeLatch()
+ const r = Registry.make()
+ let i = 0
+ const rx = Rx.make(Effect.sync(() => i))
+ const optimisticRx = rx.pipe(
+ Rx.optimistic
+ )
+ const fn = optimisticRx.pipe(
+ Rx.optimisticFn({
+ reducer: (_current, update: number) => Result.success(update),
+ fn: (set) =>
+ Rx.fn(Effect.fnUntraced(function*() {
+ set(Result.success(123))
+ yield* latch.await
+ }))
+ }),
+ Rx.keepAlive
+ )
+
+ expect(r.get(rx)).toEqual(Result.success(0))
+ assert.deepStrictEqual(r.get(optimisticRx), Result.success(0))
+ r.set(fn, 1)
+ i = 2
+
+ // optimistic phase: the intermediate value is set, but the true value is
+ // not
+ assert.deepStrictEqual(r.get(rx), Result.success(0))
+ assert.deepStrictEqual(r.get(optimisticRx), Result.success(123, { waiting: true }))
+
+ latch.unsafeOpen()
+ await Effect.runPromise(Effect.yieldNow())
+
+ // commit phase: a refresh is triggered, the authoritative value is used
+ assert.deepStrictEqual(r.get(rx), Result.success(2))
+ assert.deepStrictEqual(r.get(optimisticRx), Result.success(2))
+ })
})
})