Skip to content

Commit

Permalink
Add multi-read exported read function.
Browse files Browse the repository at this point in the history
  • Loading branch information
rkirov committed Mar 3, 2023
1 parent 7f31d24 commit 449923e
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 28 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,6 @@ I consider this library still a work-in-progress. My next tasks would be:
- [ ] add more tests, I still have doubts about the correctness of the algorithms used.
- [ ] more efficient aggregation of inputs and invalidation like Angular Signals.
- [ ] add support for propagating thrown errors during recomputation.
- [ ] add support for 'multi-read', something like `read([xS,yS], (x,y) => {...})`. While `x.read(x => y.read(y => {...}))` works, it recreates the inner signal on each recomputation for `x`. In a way the library, assumes `y` was read as by-product of the specific value for `x`, so it has to be recreated. But there is
no current way to declare `read x and y independent of their values`.
- [ ] benchmark performance and memory usage. It was not an explicit goal of the current implementation, but there could be benefits of the minimal continuation approach.
- [ ] Make sure there are no mem leaks. I used WeakRef, but didn't test that.
- [ ] add effects, i.e. computations that are recomputed without an explicit .value read.
Expand Down
23 changes: 22 additions & 1 deletion basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {input} from '.';
import {input, read} from '.';

test('basic signal', () => {
const a = input(1);
Expand Down Expand Up @@ -42,3 +42,24 @@ test('read returning signal', () => {
c.value = true;
expect(res.value).toBe(1);
});

test('multi read returning value', () => {
const a = input(1);
const b = input(2);
const c = read(a, b, (a, b) => a + b);
expect(c.value).toBe(3);
a.value = 5;
expect(c.value).toBe(7);
b.value = 10;
expect(c.value).toBe(15);
});

test('multi read returning signal', () => {
const a = input(1);
const b = input(2);
const c = input(false);
const res = read(a, b, c, (a, b, c) => c ? a : b);
expect(res.value).toBe(2);
c.value = true;
expect(res.value).toBe(1);
});
68 changes: 43 additions & 25 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

export interface Signal<T> {
get value(): T;
read<S>(f: (t: T) => S|Signal<S>, debugName?: string): Signal<S>;
read<S>(f: (t: T) => S | Signal<S>, debugName?: string): Signal<S>;
}

export interface Input<T> extends Signal<T> {
Expand Down Expand Up @@ -57,7 +57,7 @@ type ContValue<T> = (ct: Continuation<T>, curState: State) => void;
/**
* Global count of signals created. Used for debugging only.
*/
let COUNT = 0;
let COUNT = 0;

class SignalImpl<T> implements Signal<T> {
/**
Expand Down Expand Up @@ -90,28 +90,8 @@ class SignalImpl<T> implements Signal<T> {
* Name is optional, used for debugging only.
*/
constructor(private ct: ContValue<T>, private name?: string) { }
read<S>(f: (t: T) => S|SignalImpl<S>, name?: string): SignalImpl<S> {
return new SignalImpl<S>((ct, curState) => {
const val = this.value;
if (this.state === State.CLEAN_AND_SAME_VALUE && curState !== State.INITIAL) {
// the first two values don't matter, because we are not going to use them.
// TODO: find a better way to do this.
ct(null as any, null as any, this.state);
return;
}

globalState = GLOBAL_STATE.COMPUTING;
const res = f(val);
globalState = GLOBAL_STATE.READY;

// Adding auto-wrapping of pure values, akin to JS promises.
// This means we can never create Signal<Signal<T>>.
// Are we trading off some capabilities for syntactic convenience?
if (!(res instanceof SignalImpl)) return ct(res, this.inputs, this.state);

const resV = res.value;
ct(resV, new Set([...this.inputs, ...res.inputs]), this.state);
}, name);
read<S>(f: (t: T) => S | SignalImpl<S>, name?: string): SignalImpl<S> {
return new SignalImpl<S>(makeCt([this], f), name);
}

/**
Expand Down Expand Up @@ -163,7 +143,7 @@ class InputImpl<T> extends SignalImpl<T> {
readers: Set<WeakRef<SignalImpl<any>>> = new Set();

constructor(private val: T, name?: string) {
super(_ => {throw new Error(`error: inputs continuation shouldn't be called`)}, name);
super(_ => { throw new Error(`error: inputs continuation shouldn't be called`) }, name);
}
get value(): T {
this.checkGlobalState();
Expand All @@ -180,3 +160,41 @@ class InputImpl<T> extends SignalImpl<T> {
}
}

function makeCt<T>(signals: SignalImpl<any>[], f: (...args: any[]) => T | SignalImpl<T>): ContValue<T> {
return (ct, curState) => {
let inputs = new Set<InputImpl<any>>();
let values = signals.map(s => s.value);
for (let s of signals) {
inputs = new Set([...inputs, ...s.inputs]);
}

if (signals.map(x => x.state).every(s => s === State.CLEAN_AND_SAME_VALUE) && curState !== State.INITIAL) {
// the first two values don't matter, because we are not going to use them.
// TODO: find a better way to do this.
ct(null as any, null as any, State.CLEAN_AND_SAME_VALUE);
return;
}

globalState = GLOBAL_STATE.COMPUTING;
let res = f(...values);
globalState = GLOBAL_STATE.READY;

// Adding auto-wrapping of pure values, akin to JS promises.
// This means we can never create Signal<Signal<T>>.
// Are we trading off some capabilities for syntactic convenience?
if (!(res instanceof SignalImpl)) return ct(res, inputs, State.CLEAN_AND_DIFFERENT_VALUE);
let resV = res.value;
ct(resV, new Set([...inputs, ...res.inputs]), res.state);
}
}

export function read<A, R>(s: Signal<A>, f: (a: A) => R | Signal<R>): Signal<R>;
export function read<A, B, R>(a: Signal<A>, b: Signal<B>, f: (a: A, b: B) => R | Signal<R>): Signal<R>;
export function read<A, B, C, R>(a: Signal<A>, b: Signal<B>, c: Signal<C>, f: (a: A, b: B, c: C) => R | Signal<R>): Signal<R>;
export function read<R>(...args: any[]): Signal<R> {
let f = args[args.length - 1] as (...args: any[]) => R | SignalImpl<R>;
let signals = args.slice(0, args.length - 1) as SignalImpl<any>[];
if (signals.length === 1) return signals[0].read(f);

return new SignalImpl(makeCt(signals, f));
}

0 comments on commit 449923e

Please sign in to comment.