feat: improve async signals (#156)

This commit is contained in:
Jacob
2023-01-26 05:39:03 +01:00
committed by GitHub
parent 1bd4d84cb1
commit db27b9d5fb
7 changed files with 114 additions and 36 deletions

View File

@@ -1,6 +1,7 @@
import {computed, initial, signal} from '../decorators';
import {
Color,
PossibleVector2,
Rect as RectType,
SerializedVector2,
Vector2,
@@ -124,20 +125,37 @@ export class Image extends Rect {
);
}
public getColorAtPoint(position: Vector2): Color {
/**
* Get color of the image at the given position.
*
* @param position - The position in local space at which to sample the color.
*/
public getColorAtPoint(position: PossibleVector2): Color {
const image = this.image();
const size = this.computedSize();
const naturalSize = new Vector2(image.naturalWidth, image.naturalHeight);
const pixelPosition = new Vector2(position)
.add(this.computedSize().scale(0.5))
.mul(naturalSize.div(size).safe);
return this.getPixelColor(pixelPosition);
}
/**
* Get color of the image at the given pixel.
*
* @param position - The pixel's position.
*/
public getPixelColor(position: PossibleVector2): Color {
const context = this.filledImageCanvas();
const relativePosition = position.add(this.computedSize().scale(0.5));
const data = context.getImageData(
relativePosition.x,
relativePosition.y,
1,
1,
).data;
const vector = new Vector2(position);
const data = context.getImageData(vector.x, vector.y, 1, 1).data;
return new Color({
r: data[0] / 255,
g: data[1] / 255,
b: data[2] / 255,
r: data[0],
g: data[1],
b: data[2],
a: data[3] / 255,
});
}

View File

@@ -948,20 +948,6 @@ export class Node implements Promisable<Node> {
return hit;
}
/**
* Wait for any asynchronous resources that this node or its children have.
*
* @remarks
* Certain resources like images are always loaded asynchronously.
* Awaiting this method makes sure that all such resources are done loading
* before continuing the animation.
*/
public waitForAsyncResources() {
this.collectAsyncResources();
const promises = DependencyContext.consumePromises();
return Promise.all(promises.map(handle => handle.promise));
}
/**
* Collect all asynchronous resources used by this node.
*/
@@ -971,8 +957,21 @@ export class Node implements Promisable<Node> {
}
}
/**
* Wait for any asynchronous resources that this node or its children have.
*
* @remarks
* Certain resources like images are always loaded asynchronously.
* Awaiting this method makes sure that all such resources are done loading
* before continuing the animation.
*/
public async toPromise(): Promise<this> {
await this.waitForAsyncResources();
let promises = DependencyContext.consumePromises();
do {
await Promise.all(promises.map(handle => handle.promise));
this.collectAsyncResources();
promises = DependencyContext.consumePromises();
} while (promises.length > 0);
return this;
}

View File

@@ -177,6 +177,8 @@ export class Video extends Rect {
protected setCurrentTime(value: number) {
const video = this.video();
if (video.readyState < 2) return;
video.currentTime = value;
this.lastTime = value;
if (video.seeking) {

View File

@@ -1,5 +1,6 @@
import {FlagDispatcher, Subscribable} from '../events';
import {DetailedError} from '../utils';
import {Promisable} from '../threading';
export interface PromiseHandle<T> {
promise: Promise<T>;
@@ -8,7 +9,9 @@ export interface PromiseHandle<T> {
owner?: any;
}
export class DependencyContext<TOwner = void> {
export class DependencyContext<TOwner = void>
implements Promisable<DependencyContext<TOwner>>
{
protected static collectionSet = new Set<DependencyContext<any>>();
protected static collectionStack: DependencyContext<any>[] = [];
protected static promises: PromiseHandle<any>[] = [];
@@ -28,7 +31,7 @@ export class DependencyContext<TOwner = void> {
stack: new Error().stack,
};
const context = this.collectionStack.at(-2);
const context = this.collectionStack.at(-1);
if (context) {
handle.owner = context.owner;
}
@@ -59,6 +62,10 @@ export class DependencyContext<TOwner = void> {
Object.defineProperty(this.invokable, 'context', {
value: this,
});
Object.defineProperty(this.invokable, 'toPromise', {
value: this.toPromise.bind(this),
});
}
protected invoke() {
@@ -92,4 +99,14 @@ export class DependencyContext<TOwner = void> {
this.event.subscribe(signal.markDirty);
}
}
public async toPromise(): Promise<this> {
let promises = DependencyContext.consumePromises();
do {
await Promise.all(promises.map(handle => handle.promise));
this.invokable();
promises = DependencyContext.consumePromises();
} while (promises.length > 0);
return this.invokable;
}
}

View File

@@ -1,6 +1,7 @@
import {describe, test, expect} from 'vitest';
import {createSignal} from './createSignal';
import {createComputedAsync} from './createComputedAsync';
import {createSignal} from './createSignal';
function sleep(duration = 0) {
return new Promise(resolve => setTimeout(resolve, duration));
@@ -8,7 +9,10 @@ function sleep(duration = 0) {
describe('createComputedAsync()', () => {
test('Value is updated when the promise resolves', async () => {
const computed = createComputedAsync(() => sleep().then(() => true), false);
const computed = createComputedAsync(async () => {
await sleep();
return true;
}, false);
const signal = createSignal(() => computed());
expect(signal()).toBe(false);
@@ -17,4 +21,24 @@ describe('createComputedAsync()', () => {
expect(signal()).toBe(true);
});
test('Value is updated when its dependencies change', async () => {
const dependency = createSignal(2);
const computed = createComputedAsync(async () => {
const value = dependency();
await sleep();
return value;
}, 1);
const signal = createSignal(() => computed());
expect(signal()).toBe(1);
await sleep(1);
expect(signal()).toBe(2);
dependency(3);
expect(signal()).toBe(2);
await sleep(1);
expect(signal()).toBe(3);
});
});

View File

@@ -1,5 +1,10 @@
import {createComputed} from './createComputed';
import {Computed, ComputedContext} from '../signals';
import {
Computed,
ComputedContext,
createSignal,
PromiseHandle,
} from '../signals';
export function createComputedAsync<T>(
factory: () => Promise<T>,
@@ -12,8 +17,17 @@ export function createComputedAsync<T>(
factory: () => Promise<T>,
initial: T | null = null,
): Computed<T | null> {
const handle = createComputed(() =>
ComputedContext.collectPromise(factory(), initial),
);
return createComputed(() => handle().value);
let handle: PromiseHandle<T | null>;
const signal = createSignal(factory);
return createComputed(() => {
const promise = signal();
if (!handle || handle.promise !== promise) {
handle = ComputedContext.collectPromise(
promise,
handle?.value ?? initial,
);
}
return handle.value;
});
}

View File

@@ -5,7 +5,11 @@ export interface Promisable<T> {
}
export function isPromisable(value: any): value is Promisable<any> {
return value && typeof value === 'object' && 'toPromise' in value;
return (
value &&
(typeof value === 'object' || typeof value === 'function') &&
'toPromise' in value
);
}
/**