Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add asyncComputed, asyncEffect #24

Merged
merged 8 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/unstable/asyncComputed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Computed } from '../computed.js';
import { nextTrackId } from '../effect.js';
import { Dependency, endTrack, link, shallowPropagate, startTrack, SubscriberFlags } from '../system.js';
import { asyncCheckDirty } from './asyncSystem.js';

export function asyncComputed<T>(getter: (cachedValue?: T) => AsyncGenerator<Dependency, T>): AsyncComputed<T> {
return new AsyncComputed<T>(getter);
}

export class AsyncComputed<T = any> extends Computed {

async get(): Promise<T> {
const flags = this.flags;
if (flags & SubscriberFlags.Dirty) {
if (await this.update()) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
} else if (flags & SubscriberFlags.ToCheckDirty) {
if (await asyncCheckDirty(this.deps!)) {
if (await this.update()) {
const subs = this.subs;
if (subs !== undefined) {
shallowPropagate(subs);
}
}
} else {
this.flags = flags & ~SubscriberFlags.ToCheckDirty;
}
}
return this.currentValue!;
}

// @ts-expect-error
async update(): Promise<boolean> {
try {
startTrack(this);
const trackId = nextTrackId();
const oldValue = this.currentValue;
const generator = this.getter(oldValue);
let current = await generator.next();
while (!current.done) {
const dep = current.value;
if (dep.lastTrackedId !== trackId) {
dep.lastTrackedId = trackId;
link(dep, this);
}
current = await generator.next();

// if (this.flags & SubscriberFlags.Recursed) {
// return await this.get() !== oldValue;
// }
}
const newValue = await current.value;
if (oldValue !== newValue) {
this.currentValue = newValue;
return true;
}
return false;
} finally {
endTrack(this);
}
}
}
59 changes: 59 additions & 0 deletions src/unstable/asyncEffect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Effect, nextTrackId } from '../effect.js';
import { Dependency, endTrack, link, startTrack, SubscriberFlags } from '../system.js';
import { asyncCheckDirty } from './asyncSystem.js';

export function asyncEffect<T>(fn: () => AsyncGenerator<Dependency, T>): AsyncEffect<T> {
const e = new AsyncEffect(fn);
e.run();
return e;
}

export class AsyncEffect<T = any> extends Effect {

async notify(): Promise<void> {
let flags = this.flags;
if (flags & SubscriberFlags.Dirty) {
this.run();
return;
}
if (flags & SubscriberFlags.ToCheckDirty) {
if (await asyncCheckDirty(this.deps!)) {
this.run();
return;
} else {
this.flags = flags &= ~SubscriberFlags.ToCheckDirty;
}
}
if (flags & SubscriberFlags.InnerEffectsPending) {
this.flags = flags & ~SubscriberFlags.InnerEffectsPending;
let link = this.deps!;
do {
const dep = link.dep;
if ('notify' in dep) {
dep.notify();
}
link = link.nextDep!;
} while (link !== undefined);
}
}

async run(): Promise<T> {
try {
startTrack(this);
const trackId = nextTrackId();
const generator = this.fn();
let current = await generator.next();
while (!current.done) {
const dep = current.value;
if (dep.lastTrackedId !== trackId) {
dep.lastTrackedId = trackId;
link(dep, this);
}
current = await generator.next();
}
return await current.value;
} finally {
endTrack(this);
}
}
}
72 changes: 72 additions & 0 deletions src/unstable/asyncSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { IComputed, Link, shallowPropagate, SubscriberFlags } from "../system";

export async function asyncCheckDirty(link: Link): Promise<boolean> {
let stack = 0;
let dirty: boolean;
let nextDep: Link | undefined;

top: do {
dirty = false;
const dep = link.dep;
if ('update' in dep) {
const depFlags = dep.flags;
if (depFlags & SubscriberFlags.Dirty) {
if (await dep.update()) {
const subs = dep.subs!;
if (subs.nextSub !== undefined) {
shallowPropagate(subs);
}
dirty = true;
}
} else if (depFlags & SubscriberFlags.ToCheckDirty) {
const depSubs = dep.subs!;
if (depSubs.nextSub !== undefined) {
depSubs.prevSub = link;
}
link = dep.deps!;
++stack;
continue;
}
}
if (dirty || (nextDep = link.nextDep) === undefined) {
if (stack) {
let sub = link.sub as IComputed;
do {
--stack;
const subSubs = sub.subs!;
let prevLink = subSubs.prevSub!;
if (prevLink !== undefined) {
subSubs.prevSub = undefined;
if (dirty) {
if (await sub.update()) {
shallowPropagate(sub.subs!);
sub = prevLink.sub as IComputed;
continue;
}
} else {
sub.flags &= ~SubscriberFlags.ToCheckDirty;
}
} else {
if (dirty) {
if (await sub.update()) {
sub = subSubs.sub as IComputed;
continue;
}
} else {
sub.flags &= ~SubscriberFlags.ToCheckDirty;
}
prevLink = subSubs;
}
link = prevLink.nextDep!;
if (link !== undefined) {
continue top;
}
sub = prevLink.sub as IComputed;
dirty = false;
} while (stack);
}
return dirty;
}
link = nextDep;
} while (true);
}
3 changes: 3 additions & 0 deletions src/unstable/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './asyncComputed.js';
export * from './asyncEffect.js';
export * from './asyncSystem.js';
export * from './computedArray.js';
export * from './computedSet.js';
export * from './equalityComputed.js';
67 changes: 67 additions & 0 deletions tests/asyncComputed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from 'vitest';
import { unstable } from '../src';
import { signal } from './api';

const { asyncComputed, asyncEffect } = unstable;

test('should track dep after await', async () => {
const src = signal(0);
const c = asyncComputed(async function* () {
await sleep(100);
return (yield src, src).get();
});
expect(await c.get()).toBe(0);

src.set(1);
expect(await c.get()).toBe(1);
});

test('should trigger asyncEffect', async () => {
let triggerTimes = 0;

const src = signal(0);
const c = asyncComputed(async function* () {
await sleep(100);
return (yield src, src).get();
});
asyncEffect(async function* () {
triggerTimes++;
(yield c, c).get();
});
expect(triggerTimes).toBe(1);

await sleep(200);
src.set(1);
await sleep(200);
expect(triggerTimes).toBe(2);
});

test.skip('should stop calculating when dep updated', async () => {
let calcTimes = 0;

const a = signal('a0');
const b = signal('b0');
const c = asyncComputed(async function* () {
calcTimes++;
const v1 = (yield a, a).get();
await sleep(200);
const v2 = (yield b, b).get();
return v1 + '-' + v2;
});

expect(await c.get()).toBe('a0-b0');
expect(calcTimes).toBe(1);

a.set('a1');
const promise = c.get();
await sleep(100);
expect(calcTimes).toBe(2);
a.set('a2');

expect(await promise).toBe('a2-b0');
expect(calcTimes).toBe(3);
});

function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms));
}
Loading