feat: produceAsync
This commit is contained in:
parent
25f9992be6
commit
a0dd5c94f5
|
|
@ -2,12 +2,39 @@ import {Signal, signal, SignalOptions} from '@preact/signals-core';
|
||||||
import {create} from 'mutative';
|
import {create} from 'mutative';
|
||||||
|
|
||||||
export class MutableSignal<T> extends Signal<T> {
|
export class MutableSignal<T> extends Signal<T> {
|
||||||
|
private _interruptions: Promise<void>[] = [];
|
||||||
|
|
||||||
public constructor(t?: T, options?: SignalOptions<T>) {
|
public constructor(t?: T, options?: SignalOptions<T>) {
|
||||||
super(t, options);
|
super(t, options);
|
||||||
}
|
}
|
||||||
produce(fn: (draft: T) => void) {
|
produce(fn: (draft: T) => void) {
|
||||||
this.value = create(this.value, fn);
|
this.value = create(this.value, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个中断 Promise,`produceAsync` 会等待它完成后再更新状态。
|
||||||
|
* 通常用于传入动画 Promise,让状态更新等待动画播放完毕。
|
||||||
|
*/
|
||||||
|
addInterruption(promise: Promise<void>): void {
|
||||||
|
this._interruptions.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有未完成的中断 Promise。
|
||||||
|
*/
|
||||||
|
clearInterruptions(): void {
|
||||||
|
this._interruptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步版本的 produce。会先等待所有通过 `addInterruption` 添加的 Promise 完成,
|
||||||
|
* 然后再更新状态。适用于需要在状态更新前播放动画的场景。
|
||||||
|
*/
|
||||||
|
async produceAsync(fn: (draft: T) => void): Promise<void> {
|
||||||
|
await Promise.allSettled(this._interruptions);
|
||||||
|
this._interruptions = [];
|
||||||
|
this.produce(fn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mutableSignal<T>(initial?: T, options?: SignalOptions<T>): MutableSignal<T> {
|
export function mutableSignal<T>(initial?: T, options?: SignalOptions<T>): MutableSignal<T> {
|
||||||
|
|
|
||||||
|
|
@ -121,4 +121,102 @@ describe('MutableSignal', () => {
|
||||||
expect(s.value.count).toBe(2);
|
expect(s.value.count).toBe(2);
|
||||||
expect(s.value.items).toEqual([1, 2, 3, 4]);
|
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<void>(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<void>(resolve => {
|
||||||
|
setTimeout(() => { order.push('anim1'); resolve(); }, 20);
|
||||||
|
});
|
||||||
|
const anim2 = new Promise<void>(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<void>(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<void>(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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue