feat: produceAsync

This commit is contained in:
hypercross 2026-04-04 15:08:34 +08:00
parent 25f9992be6
commit a0dd5c94f5
2 changed files with 125 additions and 0 deletions

View File

@ -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> {

View File

@ -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 先完成10msanim1 后完成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);
});
});
}); });