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

Strict property initialization #78

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/coverage
/dist
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-empty-function": "off"
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "error"
}
};
93 changes: 60 additions & 33 deletions src/behavior.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { cons, DoubleLinkedList, Node, fromArray, nil } from "./datastructures";
import { combine, isPlaceholder } from "./index";
import { State, Reactive, Time, BListener, Parent, SListener } from "./common";
import { Future, BehaviorFuture } from "./future";
import {
Cons,
cons,
DoubleLinkedList,
fromArray,
nil,
Node
} from "./datastructures";
import {
__UNSAFE_GET_LAST_BEHAVIOR_VALUE,
combine,
isPlaceholder
} from "./index";
import { BListener, Parent, Reactive, SListener, State, Time } from "./common";
import * as F from "./future";
import { BehaviorFuture, Future } from "./future";
import {
Stream,
FlatFuturesOrdered,
FlatFutures,
FlatFuturesLatest,
FlatFutures
FlatFuturesOrdered,
Stream
} from "./stream";
import { tick, getTime } from "./clock";
import { sample, Now } from "./now";
import { getTime, tick } from "./clock";
import { Now, sample } from "./now";

export type MapBehaviorTuple<A> = { [K in keyof A]: Behavior<A[K]> };

Expand All @@ -22,7 +33,7 @@ export type MapBehaviorTuple<A> = { [K in keyof A]: Behavior<A[K]> };
export abstract class Behavior<A> extends Reactive<A, BListener>
implements Parent<BListener> {
// Behaviors cache their last value in `last`.
last: A;
last?: A;
children: DoubleLinkedList<BListener> = new DoubleLinkedList();
pulledAt: number | undefined;
changedAt: number | undefined;
Expand Down Expand Up @@ -70,7 +81,7 @@ export abstract class Behavior<A> extends Reactive<A, BListener>
const time = t === undefined ? tick() : t;
this.pull(time);
}
return this.last;
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(this);
}
abstract update(t: number): A;
pushB(t: number): void {
Expand All @@ -88,7 +99,11 @@ export abstract class Behavior<A> extends Reactive<A, BListener>
for (const parent of this.parents) {
if (isBehavior(parent)) {
parent.pull(t);
shouldRefresh = shouldRefresh || this.changedAt < parent.changedAt;
shouldRefresh =
shouldRefresh ||
(this.changedAt !== undefined &&
parent.changedAt !== undefined &&
this.changedAt < parent.changedAt);
}
}
if (shouldRefresh) {
Expand Down Expand Up @@ -139,7 +154,7 @@ function refresh<A>(b: Behavior<A>, t: number) {

export function isBehavior<A>(b: unknown): b is Behavior<A> {
return (
(typeof b === "object" && "at" in b && !isPlaceholder(b)) ||
(typeof b === "object" && b !== null && "at" in b && !isPlaceholder(b)) ||
(isPlaceholder(b) && (b.source === undefined || isBehavior(b.source)))
);
}
Expand Down Expand Up @@ -184,14 +199,16 @@ class ProducerBehaviorFromFunction<A> extends ProducerBehavior<A> {
) {
super();
}
deactivateFn: () => void;
deactivateFn?: () => void;
activateProducer(): void {
this.state = State.Push;
this.deactivateFn = this.activateFn(this.newValue.bind(this));
}
deactivateProducer(): void {
this.state = State.Inactive;
this.deactivateFn();
if (this.deactivateFn !== undefined) {
this.deactivateFn();
}
}
}

Expand Down Expand Up @@ -243,7 +260,7 @@ export class MapBehavior<A, B> extends Behavior<B> {
this.parents = cons(parent);
}
update(_t: number): B {
return this.f(this.parent.last);
return this.f(__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.parent));
}
}

Expand All @@ -253,7 +270,9 @@ class ApBehavior<A, B> extends Behavior<B> {
this.parents = cons<Behavior<((a: A) => B) | A>>(fn, cons(val));
}
update(_t: number): B {
return this.fn.last(this.val.last);
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.fn)(
__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.val)
);
}
}

Expand Down Expand Up @@ -295,19 +314,22 @@ export class LiftBehavior<A extends unknown[], R> extends Behavior<R> {

class FlatMapBehavior<A, B> extends Behavior<B> {
// The last behavior returned by the chain function
private innerB: Behavior<B>;
private innerB?: Behavior<B> | undefined;
private innerNode: Node<this> = new Node(this);
constructor(private outer: Behavior<A>, private fn: (a: A) => Behavior<B>) {
super();
this.parents = cons(this.outer);
}
update(t: number): B {
const outerChanged = this.outer.changedAt > this.changedAt;
const outerChanged =
this.outer.changedAt !== undefined &&
this.changedAt !== undefined &&
this.outer.changedAt > this.changedAt;
if (outerChanged || this.changedAt === undefined) {
if (this.innerB !== undefined) {
this.innerB.removeListener(this.innerNode);
}
this.innerB = this.fn(this.outer.last);
this.innerB = this.fn(__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.outer));
this.innerB.addListener(this.innerNode, t);
if (this.state !== this.innerB.state) {
this.changeStateDown(this.innerB.state);
Expand All @@ -317,7 +339,11 @@ class FlatMapBehavior<A, B> extends Behavior<B> {
this.innerB.pull(t);
}
}
return this.innerB.last;
if (this.innerB === undefined) {
// panic!
throw new Error("FlatMapBehavior#innerB should be defined");
}
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.innerB);
}
}

Expand Down Expand Up @@ -359,7 +385,7 @@ class SnapshotBehavior<A> extends Behavior<Future<A>> implements SListener<A> {
}
}
pushS(t: number, _val: A): void {
this.last.resolve(this.parent.at(t), t);
__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this).resolve(this.parent.at(t), t);
this.parents = cons(this.parent);
this.changeStateDown(this.state);
this.parent.addListener(this.node, t);
Expand All @@ -368,7 +394,7 @@ class SnapshotBehavior<A> extends Behavior<Future<A>> implements SListener<A> {
if (this.future.state === State.Done) {
return Future.of(this.parent.at(t));
} else {
return this.last;
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(this);
}
}
}
Expand Down Expand Up @@ -440,7 +466,7 @@ class SwitcherBehavior<A> extends ActiveBehavior<A>
next.addListener(this.nNode, t);
}
update(_t: Time): A {
return this.b.last;
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.b);
}
pushS(t: number, value: Behavior<A>): void {
this.doSwitch(t, value);
Expand Down Expand Up @@ -662,7 +688,7 @@ export type SampleAt = <B>(b: Behavior<B>) => B;

class MomentBehavior<A> extends Behavior<A> {
private sampleBound: SampleAt;
private currentSampleTime: Time;
private currentSampleTime?: Time;
constructor(private f: (at: SampleAt) => A) {
super();
this.sampleBound = (b) => this.sample(b);
Expand Down Expand Up @@ -703,17 +729,18 @@ class MomentBehavior<A> extends Behavior<A> {
parent.removeListener(node);
}
}
this.parents = undefined;
const value = this.f(this.sampleBound);
return value;
this.parents = nil;
return this.f(this.sampleBound);
}
sample<B>(b: Behavior<B>): B {
const node = new Node(this);
this.listenerNodes = cons({ node, parent: b }, this.listenerNodes);
b.addListener(node, this.currentSampleTime);
b.at(this.currentSampleTime);
if (this.currentSampleTime !== undefined) {
b.addListener(node, this.currentSampleTime);
b.at(this.currentSampleTime);
}
this.parents = cons(b, this.parents);
return b.last;
return __UNSAFE_GET_LAST_BEHAVIOR_VALUE(b);
}
}

Expand All @@ -727,7 +754,7 @@ class FormatBehavior extends Behavior<string> {
private behaviors: Array<string | number | Behavior<string | number>>
) {
super();
let parents = undefined;
let parents: Cons<Behavior<string | number>> = nil;
for (const b of behaviors) {
if (isBehavior(b)) {
parents = cons(b, parents);
Expand All @@ -739,7 +766,7 @@ class FormatBehavior extends Behavior<string> {
let resultString = this.strings[0];
for (let i = 0; i < this.behaviors.length; ++i) {
const b = this.behaviors[i];
const value = isBehavior(b) ? b.last : b;
const value = isBehavior(b) ? __UNSAFE_GET_LAST_BEHAVIOR_VALUE(b) : b;
resultString += value.toString() + this.strings[i + 1];
}
return resultString;
Expand Down
42 changes: 29 additions & 13 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { Cons, cons, DoubleLinkedList, Node } from "./datastructures";
import { Cons, cons, DoubleLinkedList, nil, Node } from "./datastructures";
import { Behavior } from "./behavior";
import { tick } from "./clock";

export type Time = number;

function isBehavior(b: unknown): b is Behavior<unknown> {
return typeof b === "object" && "at" in b;
return typeof b === "object" && b !== null && "at" in b;
}

export type PullHandler = (pull: (t?: number) => void) => () => void;

/**
* @internal
* Do not use!
* @throws {Error}
*/
export const __UNSAFE_GET_LAST_BEHAVIOR_VALUE = <A>(b: Behavior<A>): A => {
if (b.last === undefined) {
// panic!
throw new Error("Behavior#last value should be defined");
}
return b.last;
};

/**
* The various states that a reactive can be in. The order matters here: Done <
* Push < Pull < Inactive. The idea is that a reactive can calculate its current
Expand Down Expand Up @@ -56,7 +69,7 @@ export class PushOnlyObserver<A> implements BListener, SListener<A> {
}
}
pushB(_t: number): void {
this.callback((this.source as Behavior<A>).last);
this.callback(__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.source as Behavior<A>));
}
pushS(_t: number, value: A): void {
this.callback(value);
Expand All @@ -73,13 +86,10 @@ export type NodeParentPair = {
};

export abstract class Reactive<A, C extends Child> implements Child {
state: State;
parents: Cons<Parent<unknown>>;
state: State = State.Inactive;
parents: Cons<Parent<unknown>> = nil;
listenerNodes: Cons<NodeParentPair> | undefined;
children: DoubleLinkedList<C> = new DoubleLinkedList();
constructor() {
this.state = State.Inactive;
}
addListener(node: Node<C>, t: number): State {
const firstChild = this.children.head === undefined;
this.children.prepend(node);
Expand Down Expand Up @@ -135,18 +145,22 @@ export abstract class Reactive<A, C extends Child> implements Child {
}

export class CbObserver<A> implements BListener, SListener<A> {
private endPulling: () => void;
private endPulling?: () => void;
node: Node<CbObserver<A>> = new Node(this);
constructor(
private callback: (a: A) => void,
readonly handlePulling: PullHandler,
private time: Time,
private time: Time | undefined,
readonly source: ParentBehavior<A>
) {
source.addListener(this.node, tick());
if (source.state === State.Pull) {
this.endPulling = handlePulling(this.pull.bind(this));
} else if (isBehavior(source) && source.state === State.Push) {
} else if (
isBehavior(source) &&
source.state === State.Push &&
source.last !== undefined
) {
callback(source.last);
}
this.time = undefined;
Expand All @@ -156,11 +170,13 @@ export class CbObserver<A> implements BListener, SListener<A> {
time !== undefined ? time : this.time !== undefined ? this.time : tick();
if (isBehavior(this.source) && this.source.state === State.Pull) {
this.source.pull(t);
this.callback(this.source.last);
if (this.source.last !== undefined) {
this.callback(this.source.last);
}
}
}
pushB(_t: number): void {
this.callback((this.source as Behavior<A>).last);
this.callback(__UNSAFE_GET_LAST_BEHAVIOR_VALUE(this.source as Behavior<A>));
}
pushS(_t: number, value: A): void {
this.callback(value);
Expand Down
Loading