reactant-module
Version:
A module model for Reactant
290 lines (248 loc) • 6.38 kB
text/typescript
/* eslint-disable no-shadow */
/* eslint-disable consistent-return */
/* eslint-disable no-else-return */
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable no-use-before-define */
const enum ComputedFlags {
Running,
Notified,
Outdated,
HasError,
Tracking,
}
interface ExternalSignal<T = unknown> {
value: T;
}
interface ExternalComputed<T = unknown> extends ExternalSignal<T> {
readonly value: T;
}
interface Node {
_source: Signal;
_prevSource?: Node;
_nextSource?: Node;
_target: Computed;
_prevTarget?: Node;
_nextTarget?: Node;
_version: number;
_rollbackNode?: Node;
}
let evalContext: Computed | undefined;
let globalVersion = 0;
let untrackedDepth = 0;
const untracked = <T>(callback: () => T): T => {
if (untrackedDepth > 0) {
return callback();
}
const prevContext = evalContext;
evalContext = undefined;
untrackedDepth += 1;
try {
return callback();
} finally {
untrackedDepth -= 1;
evalContext = prevContext;
}
};
class Signal<T = unknown> {
_version = 0;
_node?: Node;
_targets?: Node;
_value?: T;
constructor(value?: T) {
this._value = value;
}
_refresh() {
return true;
}
get value() {
const node = this.addDependency();
if (node !== undefined) {
node._version = this._version;
}
return this._value;
}
set value(value) {
if (evalContext instanceof Computed) {
throw new Error('Computed cannot have side-effects');
}
if (value !== this._value) {
this._value = value;
this._version += 1;
globalVersion += 1;
}
}
addDependency(): Node | undefined {
if (evalContext === undefined) {
return;
}
let node = this._node;
if (node === undefined || node._target !== evalContext) {
node = {
_version: 0,
_source: this,
_prevSource: evalContext._sources,
_nextSource: undefined,
_target: evalContext,
_prevTarget: undefined,
_nextTarget: undefined,
_rollbackNode: node,
};
if (evalContext._sources !== undefined) {
evalContext._sources._nextSource = node;
}
evalContext._sources = node;
this._node = node;
return node;
} else if (node._version === -1) {
node._version = 0;
if (node._nextSource !== undefined) {
node._nextSource._prevSource = node._prevSource;
if (node._prevSource !== undefined) {
node._prevSource._nextSource = node._nextSource;
}
node._prevSource = evalContext._sources;
node._nextSource = undefined;
evalContext._sources!._nextSource = node;
evalContext._sources = node;
}
return node;
}
}
}
const signal = <T>(value: T) => new Signal(value) as ExternalSignal<T>;
class Computed<T = unknown> extends Signal<T> {
_compute: () => T;
_sources?: Node;
_globalVersion = globalVersion - 1;
// TODO: refactor for improving performance
_flags: Set<ComputedFlags> = new Set([ComputedFlags.Outdated]);
constructor(compute: () => T) {
super(undefined);
this._compute = compute;
}
_refresh() {
this._flags.delete(ComputedFlags.Notified);
if (this._flags.has(ComputedFlags.Running)) {
return false;
}
if (
!this._flags.has(ComputedFlags.Outdated) &&
this._flags.has(ComputedFlags.Tracking)
) {
return true;
}
this._flags.delete(ComputedFlags.Outdated);
if (this._globalVersion === globalVersion) {
return true;
}
this._globalVersion = globalVersion;
this._flags.add(ComputedFlags.Running);
if (this._version > 0 && !this.needsToRecompute()) {
this._flags.delete(ComputedFlags.Running);
return true;
}
const prevContext = evalContext;
try {
this.prepareSources();
evalContext = this;
const value = this._compute();
if (
this._flags.has(ComputedFlags.HasError) ||
this._value !== value ||
this._version === 0
) {
this._value = value;
this._flags.delete(ComputedFlags.HasError);
this._version += 1;
}
} catch (err) {
this._value = err as T;
this._flags.add(ComputedFlags.HasError);
this._version += 1;
}
evalContext = prevContext;
this.cleanupSources();
this._flags.delete(ComputedFlags.Running);
return true;
}
get value() {
if (this._flags.has(ComputedFlags.Running)) {
throw new Error('Cycle detected');
}
const node = this.addDependency();
this._refresh();
if (node !== undefined) {
node._version = this._version;
}
if (this._flags.has(ComputedFlags.HasError)) {
throw this._value;
}
return this._value;
}
needsToRecompute() {
for (
let node = this._sources;
node !== undefined;
node = node._nextSource
) {
if (
node._source._version !== node._version ||
!node._source._refresh() ||
node._source._version !== node._version
) {
return true;
}
}
return false;
}
prepareSources() {
for (
let node = this._sources;
node !== undefined;
node = node._nextSource
) {
const rollbackNode = node._source._node;
if (rollbackNode !== undefined) {
node._rollbackNode = rollbackNode;
}
node._source._node = node;
node._version = -1;
if (node._nextSource === undefined) {
this._sources = node;
break;
}
}
}
cleanupSources() {
let node = this._sources;
let head: Node | undefined;
while (node !== undefined) {
const prev = node._prevSource;
if (node._version === -1) {
if (prev !== undefined) {
prev._nextSource = node._nextSource;
}
if (node._nextSource !== undefined) {
node._nextSource._prevSource = prev;
}
} else {
head = node;
}
node._source._node = node._rollbackNode;
if (node._rollbackNode !== undefined) {
node._rollbackNode = undefined;
}
node = prev;
}
this._sources = head;
}
}
const computed = <T>(compute: () => T) =>
new Computed(compute) as ExternalComputed<T>;
export {
signal,
computed,
untracked,
ExternalComputed as Computed,
ExternalSignal as Signal,
};