Skip to content
Draft
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
276 changes: 276 additions & 0 deletions src/Autorun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
/* eslint-disable */

import { Effect, batch } from "./effect";
import { STATE_DIRTY, STATE_DISPOSED } from "./constants";
import { Computation, ERROR_BIT } from "./core";
import { runWithOwner } from "./index";

export class Autorun extends Effect {
_paused = false;
_ancestor_paused = false;
_cleanup: (() => void) | undefined;
_run_once: boolean;

constructor(
fn: () => void,
cleanup?: (() => void) | undefined,
run_once = false
) {
super(undefined, fn);
this._cleanup = cleanup;
this._run_once = run_once;
}
invalidate() {
if (this._run_once) return;
batch(() => {
this._notify(STATE_DIRTY);
this._updateIfNecessary();
});
}
destroy() {
this.dispose(true);
}

pause() {
this._paused = true;
this.notify_pause(true);
}

notify_pause(pause: boolean) {
let current = this._nextSibling as Computation | null;

while (current && current._parent === this) {
if (current instanceof Autorun) {
if (!current._paused && current._ancestor_paused && !pause) {
current._ancestor_paused = false;
current._updateIfNecessary();
} else {
current._ancestor_paused = pause;
}
if (current._paused != pause) {
current.notify_pause(pause);
}
}

current = current._nextSibling as Computation;
}
}

unpause() {
this._paused = false;
if (this._ancestor_paused) return;

this.notify_pause(false);
this._updateIfNecessary();
}

override _updateIfNecessary(): void {
if (this._paused || this._ancestor_paused) return;
super._updateIfNecessary();
}

override dispose(self = true) {
if (this._state == STATE_DISPOSED) return;
if (this._cleanup && self) this._cleanup();
super.dispose(self);
}
set_run_immediately(value: boolean) {}
alive() {}
run_me() {
this._updateIfNecessary();
return this;
}
destroy_subs() {}
add_sub() {}
stop_tracking() {}

get _is_destroyed() {
return this._state == STATE_DISPOSED;
}
}

export class Box<T> extends Computation {
constructor(value: T) {
super(value, null);
}
set(v: T) {
batch(() => {
this.write(v);
});
}
get(): T {
return this.read();
}
}

export class Watcher<T> extends Computation<T> {
constructor(fn: () => T, options: {}) {
super(undefined, fn);
}
get() {
return this.wait();
}

get_ready_key() {
return undefined as unknown;
}

once_value(handler: (v: T) => void) {
handler(this.wait());
}

dont_track_scheduling() {}

invalidate() {
this._notify(STATE_DIRTY);
this._updateIfNecessary();
}

max_expected_time() {
return 0;
}

get_current(no_throw?: boolean) {
if (this._isLoading()) {
return [false, this._promise as unknown] as [false, unknown];
} else {
return [true, this.read()] as [true, T];
}
}

get_current_status() {
if (this._isLoading()) {
return { status: "not_ready", ready_keys: undefined };
} else if (this._stateFlags & ERROR_BIT) {
return { status: "error", error: this._value as Error };
} else {
return { status: "ready", value: this.read() };
}
}
}

export function autorun_top(fn: () => void) {
return runWithOwner(null, () => fn());
}

interface AutorunOptions {
do: () => void;
while?: () => boolean;
finally?: () => void;
}

export function autorun(fn: () => void, cleanup?: () => void): Autorun;
export function autorun(options: AutorunOptions): Autorun;
export function autorun(
fn_or_options: (() => void) | AutorunOptions,
cleanup_or_empty?: () => void
): Autorun {
const options =
typeof fn_or_options === "object"
? fn_or_options
: {
do: fn_or_options,
while: undefined,
finally: cleanup_or_empty,
};

let { do: do_fn, while: while_fn, finally: finally_fn } = options;

if (!while_fn) {
return new Autorun(do_fn, finally_fn).run_me();
} else {
return conditional_autorun(do_fn, while_fn, finally_fn);
}
}

function conditional_autorun(
do_fn: () => void,
while_fn: () => boolean,
finally_fn?: (() => void) | undefined
): Autorun {
return batch(() => {
const run = new Autorun(do_fn, () => {
finally_fn?.();
pauser.destroy();
});

const pauser = new Autorun(() => {
if (while_fn()) {
run.unpause();
} else {
run.pause();
}
}).run_me();

return run;
});
}

export class Switch extends Computation<boolean> {
_destroyed: boolean;
_resolve: ((v: boolean) => void) | undefined;
override _promise: Promise<boolean> = new Promise((resolve) => {
this._resolve = resolve;
});

constructor(name: string) {
super(false, null);
this._name = name;
this._destroyed = false;
}

get name() {
return this._name;
}

is_turned(): boolean {
return this._value!;
}

is_dead(): boolean {
return this._destroyed;
}

max_expected_time(): number | null {
return null;
}

turn_off(): void {
if (!this._value) {
return;
}

batch(() => {
this.write(false);
this._promise = new Promise((resolve) => {
this._resolve = resolve;
});
});
// this._was_updated()
}

turn_on(): void {
if (this._value) {
return;
}
batch(() => {
this.write(true);
this._resolve!(true);
});
// this._was_updated()
}

// Indicates that the switch is permanently turned and no longer needed
destroy(): void {
this._destroyed = true;
if (!this._value) {
this.turn_on();
} else {
this.dispose(true);
}
}

promise(): boolean {
return this.wait();
}
}
26 changes: 15 additions & 11 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ let newSources: SourceType[] | null = null;
let newSourcesIndex = 0;
let newLoadingState = false;

export const hooks = {
batch: (fn: () => void) => {
fn();
},
};

/** Computation threw a value during execution */
const ERROR_BIT = 1;
export const ERROR_BIT = 1;
/** Computation's ancestors have a unresolved promise */
const WAITING_BIT = 2;

Expand Down Expand Up @@ -127,12 +133,14 @@ export class Computation<T = any>
(value) => {
// Writing a new value (that is not a promise) will automatically
// update the state to "no longer loading"
if (this._promise === initialValue) this.write(value);
if (this._promise === initialValue)
hooks.batch(() => this.write(value));
},
(e) => {
// When the promise errors, we need to set an error state so that future reads of this
// computation will re-throw the error (until a new value is written/recomputed)
if (this._promise === initialValue) this._setError(e);
if (this._promise === initialValue)
hooks.batch(() => this._setError(e));
}
);
} else {
Expand All @@ -142,7 +150,7 @@ export class Computation<T = any>

// Used when debugging the graph; it is often helpful to know the names of sources/observers
if (__DEV__)
this._name = options?.name ?? (this._compute ? "computed" : "signal");
this._name = options?.name ?? (this._compute ? "computed" : "signal");

if (options?.equals !== undefined) this._equals = options.equals;
}
Expand Down Expand Up @@ -218,7 +226,7 @@ export class Computation<T = any>
}

/** Update the computation with a new value or promise */
write(value: T | Promise<T>): T {
write(value: T | Promise<T>): void {
if (isPromise(value)) {
// We are about to change the async state to true, and we want to notify _loading observers
// if the loading state changes. Thus, we just need to check if the current state is not
Expand All @@ -231,10 +239,10 @@ export class Computation<T = any>
// When the promise resolves, we need to update our value (or error if the promise rejects)
value.then(
(v) => {
if (this._promise === value) this.write(v);
if (this._promise === value) hooks.batch(() => this.write(v));
},
(e) => {
if (this._promise === value) this._setError(e);
if (this._promise === value) hooks.batch(() => this._setError(e));
}
);
} else if (this._equals === false || !this._equals(this._value!, value)) {
Expand Down Expand Up @@ -262,10 +270,6 @@ export class Computation<T = any>
}
}
}

// We return the value so that .write can be used in an expression
// (although it is not usually recommended)
return this._value!;
}

/**
Expand Down
Loading