Skip to content

Commit f64c25f

Browse files
authored
allow intermediate updates in optimisticFn (#265)
1 parent 06ebe82 commit f64c25f

4 files changed

Lines changed: 104 additions & 14 deletions

File tree

.changeset/fruity-years-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect-rx/rx": patch
3+
---
4+
5+
allow intermediate updates in optimisticFn

docs/rx/Rx.ts.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Added in v1.0.0
4040
- [batching](#batching)
4141
- [batch](#batch)
4242
- [combinators](#combinators)
43+
- [autoDispose](#autodispose)
4344
- [debounce](#debounce)
4445
- [initialValue](#initialvalue)
4546
- [keepAlive](#keepalive)
@@ -243,9 +244,7 @@ Added in v1.0.0
243244
**Signature**
244245
245246
```ts
246-
export declare const optimistic: <A>(
247-
self: Rx<A>
248-
) => Writable<A, Rx<Result.Result<A extends Result.Result<infer _A, infer _E> ? _A : A, unknown>>>
247+
export declare const optimistic: <A>(self: Rx<A>) => Writable<A, Rx<Result.Result<A, unknown>>>
249248
```
250249
251250
Added in v1.0.0
@@ -256,15 +255,19 @@ Added in v1.0.0
256255
257256
```ts
258257
export declare const optimisticFn: {
259-
<A, W, XA, XE, OW = A extends Result.Result<infer _A, infer _E> ? _A : A>(options: {
260-
readonly updateToValue: (value: OW, current: NoInfer<A>) => NoInfer<W>
261-
readonly fn: RxResultFn<NoInfer<OW>, XA, XE>
258+
<A, W, XA, XE, OW = W>(options: {
259+
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
260+
readonly fn:
261+
| RxResultFn<NoInfer<OW>, XA, XE>
262+
| ((set: (result: NoInfer<W>) => void) => RxResultFn<NoInfer<OW>, XA, XE>)
262263
}): (self: Writable<A, Rx<Result.Result<W, unknown>>>) => RxResultFn<OW, XA, XE>
263-
<A, W, XA, XE, OW = A extends Result.Result<infer _A, infer _E> ? _A : A>(
264+
<A, W, XA, XE, OW = W>(
264265
self: Writable<A, Rx<Result.Result<W, unknown>>>,
265266
options: {
266-
readonly updateToValue: (value: OW, current: NoInfer<A>) => NoInfer<W>
267-
readonly fn: RxResultFn<NoInfer<OW>, XA, XE>
267+
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
268+
readonly fn:
269+
| RxResultFn<NoInfer<OW>, XA, XE>
270+
| ((set: (result: NoInfer<W>) => void) => RxResultFn<NoInfer<OW>, XA, XE>)
268271
}
269272
): RxResultFn<OW, XA, XE>
270273
}
@@ -353,6 +356,21 @@ Added in v1.0.0
353356
354357
# combinators
355358
359+
## autoDispose
360+
361+
Reverts the `keepAlive` behavior of a reactive value, allowing it to be
362+
disposed of when not in use.
363+
364+
Note that Rx's have this behavior by default.
365+
366+
**Signature**
367+
368+
```ts
369+
export declare const autoDispose: <A extends Rx<any>>(self: A) => A
370+
```
371+
372+
Added in v1.0.0
373+
356374
## debounce
357375
358376
**Signature**

packages/rx/src/Rx.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,21 @@ export const keepAlive = <A extends Rx<any>>(self: A): A =>
11921192
keepAlive: true
11931193
})
11941194

1195+
/**
1196+
* Reverts the `keepAlive` behavior of a reactive value, allowing it to be
1197+
* disposed of when not in use.
1198+
*
1199+
* Note that Rx's have this behavior by default.
1200+
*
1201+
* @since 1.0.0
1202+
* @category combinators
1203+
*/
1204+
export const autoDispose = <A extends Rx<any>>(self: A): A =>
1205+
Object.assign(Object.create(Object.getPrototypeOf(self)), {
1206+
...self,
1207+
keepAlive: false
1208+
})
1209+
11951210
/**
11961211
* @since 1.0.0
11971212
* @category combinators
@@ -1433,7 +1448,9 @@ export const optimisticFn: {
14331448
<A, W, XA, XE, OW = W>(
14341449
options: {
14351450
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
1436-
readonly fn: RxResultFn<NoInfer<OW>, XA, XE>
1451+
readonly fn:
1452+
| RxResultFn<NoInfer<OW>, XA, XE>
1453+
| ((set: (result: NoInfer<W>) => void) => RxResultFn<NoInfer<OW>, XA, XE>)
14371454
}
14381455
): (
14391456
self: Writable<A, Rx<Result.Result<W, unknown>>>
@@ -1442,14 +1459,18 @@ export const optimisticFn: {
14421459
self: Writable<A, Rx<Result.Result<W, unknown>>>,
14431460
options: {
14441461
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
1445-
readonly fn: RxResultFn<NoInfer<OW>, XA, XE>
1462+
readonly fn:
1463+
| RxResultFn<NoInfer<OW>, XA, XE>
1464+
| ((set: (result: NoInfer<W>) => void) => RxResultFn<NoInfer<OW>, XA, XE>)
14461465
}
14471466
): RxResultFn<OW, XA, XE>
14481467
} = dual(2, <A, W, XA, XE, OW = W>(
14491468
self: Writable<A, Rx<Result.Result<W, unknown>>>,
14501469
options: {
14511470
readonly reducer: (current: NoInfer<A>, update: OW) => NoInfer<W>
1452-
readonly fn: RxResultFn<OW, XA, XE>
1471+
readonly fn:
1472+
| RxResultFn<NoInfer<OW>, XA, XE>
1473+
| ((set: (result: NoInfer<W>) => void) => RxResultFn<NoInfer<OW>, XA, XE>)
14531474
}
14541475
): RxResultFn<OW, XA, XE> => {
14551476
const transition = state<Result.Result<W, unknown>>(Result.initial())
@@ -1460,8 +1481,16 @@ export const optimisticFn: {
14601481
}
14611482
get.set(transition, Result.success(value, { waiting: true }))
14621483
get.set(self, transition)
1463-
get.set(options.fn, arg)
1464-
return Effect.onExit(get.result(options.fn, { suspendOnWaiting: true }), (exit) => {
1484+
const fn = typeof options.fn === "function"
1485+
? autoDispose(options.fn((value) =>
1486+
get.set(
1487+
transition,
1488+
Result.success(Result.isResult(value) ? Result.waiting(value) : value, { waiting: true })
1489+
)
1490+
))
1491+
: options.fn
1492+
get.set(fn, arg)
1493+
return Effect.onExit(get.result(fn, { suspendOnWaiting: true }), (exit) => {
14651494
get.set(transition, Result.fromExit(Exit.as(exit, value)))
14661495
return Effect.void
14671496
})

packages/rx/test/Rx.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,44 @@ describe("Rx", () => {
11711171
expect(r.get(rx)).toEqual(2)
11721172
expect(r.get(optimisticRx)).toEqual(2)
11731173
})
1174+
1175+
it("intermediate updates", async () => {
1176+
const latch = Effect.unsafeMakeLatch()
1177+
const r = Registry.make()
1178+
let i = 0
1179+
const rx = Rx.make(Effect.sync(() => i))
1180+
const optimisticRx = rx.pipe(
1181+
Rx.optimistic
1182+
)
1183+
const fn = optimisticRx.pipe(
1184+
Rx.optimisticFn({
1185+
reducer: (_current, update: number) => Result.success(update),
1186+
fn: (set) =>
1187+
Rx.fn(Effect.fnUntraced(function*() {
1188+
set(Result.success(123))
1189+
yield* latch.await
1190+
}))
1191+
}),
1192+
Rx.keepAlive
1193+
)
1194+
1195+
expect(r.get(rx)).toEqual(Result.success(0))
1196+
assert.deepStrictEqual(r.get(optimisticRx), Result.success(0))
1197+
r.set(fn, 1)
1198+
i = 2
1199+
1200+
// optimistic phase: the intermediate value is set, but the true value is
1201+
// not
1202+
assert.deepStrictEqual(r.get(rx), Result.success(0))
1203+
assert.deepStrictEqual(r.get(optimisticRx), Result.success(123, { waiting: true }))
1204+
1205+
latch.unsafeOpen()
1206+
await Effect.runPromise(Effect.yieldNow())
1207+
1208+
// commit phase: a refresh is triggered, the authoritative value is used
1209+
assert.deepStrictEqual(r.get(rx), Result.success(2))
1210+
assert.deepStrictEqual(r.get(optimisticRx), Result.success(2))
1211+
})
11741212
})
11751213
})
11761214

0 commit comments

Comments
 (0)