From a0dd5c94f52e71a02b911cb6d0c7e14939d14980 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 4 Apr 2026 15:08:34 +0800 Subject: [PATCH] feat: produceAsync --- src/utils/mutable-signal.ts | 27 ++++++++ tests/utils/mutable-signal.test.ts | 98 ++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/utils/mutable-signal.ts b/src/utils/mutable-signal.ts index 36c33d3..aaf33d2 100644 --- a/src/utils/mutable-signal.ts +++ b/src/utils/mutable-signal.ts @@ -2,12 +2,39 @@ import {Signal, signal, SignalOptions} from '@preact/signals-core'; import {create} from 'mutative'; export class MutableSignal extends Signal { + private _interruptions: Promise[] = []; + public constructor(t?: T, options?: SignalOptions) { super(t, options); } produce(fn: (draft: T) => void) { this.value = create(this.value, fn); } + + /** + * 添加一个中断 Promise,`produceAsync` 会等待它完成后再更新状态。 + * 通常用于传入动画 Promise,让状态更新等待动画播放完毕。 + */ + addInterruption(promise: Promise): void { + this._interruptions.push(promise); + } + + /** + * 清除所有未完成的中断 Promise。 + */ + clearInterruptions(): void { + this._interruptions = []; + } + + /** + * 异步版本的 produce。会先等待所有通过 `addInterruption` 添加的 Promise 完成, + * 然后再更新状态。适用于需要在状态更新前播放动画的场景。 + */ + async produceAsync(fn: (draft: T) => void): Promise { + await Promise.allSettled(this._interruptions); + this._interruptions = []; + this.produce(fn); + } } export function mutableSignal(initial?: T, options?: SignalOptions): MutableSignal { diff --git a/tests/utils/mutable-signal.test.ts b/tests/utils/mutable-signal.test.ts index fbd95ea..464877b 100644 --- a/tests/utils/mutable-signal.test.ts +++ b/tests/utils/mutable-signal.test.ts @@ -121,4 +121,102 @@ describe('MutableSignal', () => { expect(s.value.count).toBe(2); expect(s.value.items).toEqual([1, 2, 3, 4]); }); + + describe('interruption / produceAsync', () => { + it('should update immediately when no interruptions', async () => { + const s = mutableSignal({ value: 0 }); + await s.produceAsync(draft => { draft.value = 1; }); + expect(s.value.value).toBe(1); + }); + + it('should wait for addInterruption before updating', async () => { + const s = mutableSignal({ value: 0 }); + let animationDone = false; + + const animation = new Promise(resolve => { + setTimeout(() => { + animationDone = true; + resolve(); + }, 10); + }); + + s.addInterruption(animation); + const producePromise = s.produceAsync(draft => { draft.value = 42; }); + + // produceAsync 应该等待 animation 完成 + expect(animationDone).toBe(false); + expect(s.value.value).toBe(0); + + await producePromise; + expect(animationDone).toBe(true); + expect(s.value.value).toBe(42); + }); + + it('should wait for all interruptions in parallel', async () => { + const s = mutableSignal({ value: 0 }); + const order: string[] = []; + + const anim1 = new Promise(resolve => { + setTimeout(() => { order.push('anim1'); resolve(); }, 20); + }); + const anim2 = new Promise(resolve => { + setTimeout(() => { order.push('anim2'); resolve(); }, 10); + }); + + s.addInterruption(anim1); + s.addInterruption(anim2); + + await s.produceAsync(draft => { draft.value = 1; }); + + // anim2 先完成(10ms),anim1 后完成(20ms) + expect(order).toEqual(['anim2', 'anim1']); + expect(s.value.value).toBe(1); + }); + + it('should not throw when an interruption rejects', async () => { + const s = mutableSignal({ value: 0 }); + + const failingAnim = Promise.reject(new Error('animation cancelled')); + // 避免未捕获的 rejection 警告 + failingAnim.catch(() => {}); + + s.addInterruption(failingAnim); + + // allSettled 应该让 produceAsync 继续执行 + await s.produceAsync(draft => { draft.value = 99; }); + expect(s.value.value).toBe(99); + }); + + it('should clear interruptions after produceAsync resolves', async () => { + const s = mutableSignal({ value: 0 }); + + s.addInterruption(Promise.resolve()); + await s.produceAsync(draft => { draft.value = 1; }); + expect(s.value.value).toBe(1); + + // 第二次 produceAsync 不应该再等待 + await s.produceAsync(draft => { draft.value = 2; }); + expect(s.value.value).toBe(2); + }); + + it('should clear all pending interruptions manually', async () => { + const s = mutableSignal({ value: 0 }); + let animationDone = false; + + const longAnim = new Promise(resolve => { + setTimeout(() => { + animationDone = true; + resolve(); + }, 100); + }); + + s.addInterruption(longAnim); + s.clearInterruptions(); + + await s.produceAsync(draft => { draft.value = 1; }); + expect(s.value.value).toBe(1); + // clearInterruptions 后,longAnim 仍在后台运行,但 produceAsync 不会等它 + expect(animationDone).toBe(false); + }); + }); });