Comparing version 0.4.8 to 0.4.9
@@ -10,21 +10,23 @@ S = typeof S === 'undefined' ? require('..') : S; | ||
function main() { | ||
bench(createDataSignals, COUNT, COUNT); | ||
bench(createComputations0to1, COUNT, 0); | ||
bench(createComputations1to1, COUNT, COUNT); | ||
bench(createComputations2to1, COUNT, 2 * COUNT); | ||
bench(createComputations4to1, COUNT, 4 * COUNT); | ||
bench(createComputations1000to1, COUNT / 100, 10 * COUNT); | ||
//bench1(createComputations8, COUNT, 8 * COUNT); | ||
bench(createComputations1to2, COUNT, 0.5 * COUNT); | ||
bench(createComputations1to4, COUNT, 0.25 * COUNT); | ||
bench(createComputations1to8, COUNT, 0.125 * COUNT); | ||
bench(createComputations1to1000, COUNT, 0.001 * COUNT); | ||
var total = 0; | ||
total += bench(createDataSignals, COUNT, COUNT); | ||
total += bench(createComputations0to1, COUNT, 0); | ||
total += bench(createComputations1to1, COUNT, COUNT); | ||
total += bench(createComputations2to1, COUNT / 2, COUNT); | ||
total += bench(createComputations4to1, COUNT / 4, COUNT); | ||
total += bench(createComputations1000to1, COUNT / 1000, COUNT); | ||
//total += bench1(createComputations8, COUNT, 8 * COUNT); | ||
total += bench(createComputations1to2, COUNT, COUNT / 2); | ||
total += bench(createComputations1to4, COUNT, COUNT / 4); | ||
total += bench(createComputations1to8, COUNT, COUNT / 8); | ||
total += bench(createComputations1to1000, COUNT, COUNT / 1000); | ||
console.log('---'); | ||
bench(updateComputations1to1, COUNT * 4, 1); | ||
bench(updateComputations2to1, COUNT * 2, 2); | ||
bench(updateComputations4to1, COUNT, 4); | ||
bench(updateComputations1000to1, COUNT / 100, 1000); | ||
bench(updateComputations1to2, COUNT * 4, 1); | ||
bench(updateComputations1to4, COUNT * 4, 1); | ||
bench(updateComputations1to1000, COUNT * 4, 1); | ||
total += bench(updateComputations1to1, COUNT * 4, 1); | ||
total += bench(updateComputations2to1, COUNT * 2, 2); | ||
total += bench(updateComputations4to1, COUNT, 4); | ||
total += bench(updateComputations1000to1, COUNT / 100, 1000); | ||
total += bench(updateComputations1to2, COUNT * 4, 1); | ||
total += bench(updateComputations1to4, COUNT * 4, 1); | ||
total += bench(updateComputations1to1000, COUNT * 4, 1); | ||
console.log(`total: ${total.toFixed(0)}`); | ||
} | ||
@@ -34,3 +36,4 @@ | ||
var time = run(fn, count, scount); | ||
console.log(`${fn.name}: ${time}`); | ||
console.log(`${fn.name}: ${time.toFixed(0)}`); | ||
return time; | ||
} | ||
@@ -68,2 +71,3 @@ | ||
// end GC clean | ||
sources = null; | ||
%CollectGarbage(null); | ||
@@ -218,3 +222,3 @@ | ||
for (var i = 0; i < 1000; i++) { | ||
sum += ss[offset + i]; | ||
sum += ss[offset + i](); | ||
} | ||
@@ -221,0 +225,0 @@ return sum; |
@@ -7,2 +7,4 @@ export interface S { | ||
on<T>(ev: () => any, fn: (v: T) => T, seed: T, onchanges?: boolean): () => T; | ||
effect<T>(fn: () => T): void; | ||
effect<T>(fn: (v: T) => T, seed: T): void; | ||
data<T>(value: T): DataSignal<T>; | ||
@@ -13,2 +15,7 @@ value<T>(value: T, eq?: (a: T, b: T) => boolean): DataSignal<T>; | ||
cleanup(fn: (final: boolean) => any): void; | ||
isFrozen(): boolean; | ||
isListening(): boolean; | ||
makeDataNode<T>(value: T): IDataNode<T>; | ||
makeComputationNode<T>(fn: () => T): IComputationNode<T>; | ||
makeComputationNode<T>(fn: (val: T) => T, seed: T): IComputationNode<T>; | ||
} | ||
@@ -19,3 +26,15 @@ export interface DataSignal<T> { | ||
} | ||
declare const S: S; | ||
declare var S: S; | ||
export default S; | ||
export interface IDataNode<T> { | ||
clock(): IClock; | ||
current(): T; | ||
next(value: T): T; | ||
} | ||
export interface IComputationNode<T> { | ||
clock(): IClock; | ||
current(): T; | ||
} | ||
export interface IClock { | ||
time(): number; | ||
} |
152
dist/es/S.js
// Public interface | ||
var S = function S(fn, value) { | ||
var owner = Owner, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
var node = new ComputationNode(fn, value); | ||
Owner = RunningNode = node; | ||
if (RunningClock === null) { | ||
toplevelComputation(node); | ||
} | ||
else { | ||
node.value = node.fn(node.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [node]; | ||
else | ||
owner.owned.push(node); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
return function computation() { | ||
if (RunningNode !== null) { | ||
if (node.age === RootClock.time) { | ||
if (node.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
updateNode(node); // checks for state === STALE internally, so don't need to check here | ||
} | ||
logComputationRead(node, RunningNode); | ||
} | ||
return node.value; | ||
return node.current(); | ||
}; | ||
@@ -89,34 +62,13 @@ }; | ||
} | ||
S.effect = function effect(fn, value) { | ||
new ComputationNode(fn, value); | ||
}; | ||
S.data = function data(value) { | ||
var node = new DataNode(value); | ||
return function data(value) { | ||
if (arguments.length > 0) { | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { | ||
if (value !== node.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
} | ||
} | ||
else { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
} | ||
} | ||
else { | ||
if (node.log !== null) { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
event(); | ||
} | ||
else { | ||
node.value = value; | ||
} | ||
} | ||
return value; | ||
if (arguments.length === 0) { | ||
return node.current(); | ||
} | ||
else { | ||
if (RunningNode !== null) { | ||
logDataRead(node, RunningNode); | ||
} | ||
return node.value; | ||
return node.next(value); | ||
} | ||
@@ -186,2 +138,15 @@ }; | ||
}; | ||
// experimental : exposing node constructors and some state | ||
S.makeDataNode = function makeDataNode(value) { | ||
return new DataNode(value); | ||
}; | ||
S.makeComputationNode = function makeComputationNode(fn, seed) { | ||
return new ComputationNode(fn, seed); | ||
}; | ||
S.isFrozen = function isFrozen() { | ||
return RunningClock !== null; | ||
}; | ||
S.isListening = function isListening() { | ||
return RunningNode !== null; | ||
}; | ||
// Internal implementation | ||
@@ -198,2 +163,5 @@ /// Graph classes and operations | ||
}()); | ||
var RootClockProxy = { | ||
time: function () { return RootClock.time; } | ||
}; | ||
var DataNode = /** @class */ (function () { | ||
@@ -205,2 +173,35 @@ function DataNode(value) { | ||
} | ||
DataNode.prototype.current = function () { | ||
if (RunningNode !== null) { | ||
logDataRead(this, RunningNode); | ||
} | ||
return this.value; | ||
}; | ||
DataNode.prototype.next = function (value) { | ||
if (RunningClock !== null) { | ||
if (this.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== this.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + this.pending); | ||
} | ||
} | ||
else { // add to list of changes | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
} | ||
} | ||
else { // not batching, respond to change now | ||
if (this.log !== null) { | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
event(); | ||
} | ||
else { | ||
this.value = value; | ||
} | ||
} | ||
return value; | ||
}; | ||
DataNode.prototype.clock = function () { | ||
return RootClockProxy; | ||
}; | ||
return DataNode; | ||
@@ -210,4 +211,2 @@ }()); | ||
function ComputationNode(fn, value) { | ||
this.fn = fn; | ||
this.value = value; | ||
this.state = CURRENT; | ||
@@ -221,4 +220,41 @@ this.source1 = null; | ||
this.cleanups = null; | ||
this.fn = fn; | ||
this.value = value; | ||
this.age = RootClock.time; | ||
if (fn === null) | ||
return; | ||
var owner = Owner, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
Owner = RunningNode = this; | ||
if (RunningClock === null) { | ||
toplevelComputation(this); | ||
} | ||
else { | ||
this.value = this.fn(this.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [this]; | ||
else | ||
owner.owned.push(this); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
} | ||
ComputationNode.prototype.current = function () { | ||
if (RunningNode !== null) { | ||
if (this.age === RootClock.time) { | ||
if (this.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
updateNode(this); // checks for state === STALE internally, so don't need to check here | ||
} | ||
logComputationRead(this, RunningNode); | ||
} | ||
return this.value; | ||
}; | ||
ComputationNode.prototype.clock = function () { | ||
return RootClockProxy; | ||
}; | ||
return ComputationNode; | ||
@@ -338,3 +374,3 @@ }()); | ||
while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) | ||
if (count > 0) // don't tick on first run, or else we expire already scheduled updates | ||
clock.time++; | ||
@@ -341,0 +377,0 @@ clock.changes.run(applyDataChange); |
@@ -146,3 +146,3 @@ // Public interface | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { | ||
if (node.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== node.pending) { | ||
@@ -152,3 +152,3 @@ throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
} | ||
else { | ||
else { // add to list of changes | ||
markClockStale(cclock); | ||
@@ -159,3 +159,3 @@ node.pending = value; | ||
} | ||
else { | ||
else { // not batching, respond to change now | ||
if (node.log !== null) { | ||
@@ -487,3 +487,3 @@ node.pending = value; | ||
while (clock.changes.count !== 0 || clock.subclocks.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) | ||
if (count > 0) // don't tick on first run, or else we expire already scheduled updates | ||
clock.subtime++; | ||
@@ -490,0 +490,0 @@ clock.changes.run(applyDataChange); |
818
dist/S.js
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.S = factory()); | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.S = factory()); | ||
}(this, (function () { 'use strict'; | ||
// Public interface | ||
var S = function S(fn, value) { | ||
var owner = Owner, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
var node = new ComputationNode(fn, value); | ||
Owner = RunningNode = node; | ||
if (RunningClock === null) { | ||
toplevelComputation(node); | ||
// Public interface | ||
var S = function S(fn, value) { | ||
var node = new ComputationNode(fn, value); | ||
return function computation() { | ||
return node.current(); | ||
}; | ||
}; | ||
// compatibility with commonjs systems that expect default export to be at require('s.js').default rather than just require('s-js') | ||
Object.defineProperty(S, 'default', { value: S }); | ||
S.root = function root(fn) { | ||
var owner = Owner, root = fn.length === 0 ? UNOWNED : new ComputationNode(null, null), result = undefined, disposer = fn.length === 0 ? null : function _dispose() { | ||
if (RunningClock !== null) { | ||
RootClock.disposes.add(root); | ||
} | ||
else { | ||
dispose(root); | ||
} | ||
}; | ||
Owner = root; | ||
if (RunningClock === null) { | ||
result = topLevelRoot(fn, disposer, owner); | ||
} | ||
else { | ||
result = disposer === null ? fn() : fn(disposer); | ||
Owner = owner; | ||
} | ||
return result; | ||
}; | ||
function topLevelRoot(fn, disposer, owner) { | ||
try { | ||
return disposer === null ? fn() : fn(disposer); | ||
} | ||
finally { | ||
Owner = owner; | ||
} | ||
} | ||
else { | ||
node.value = node.fn(node.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [node]; | ||
else | ||
owner.owned.push(node); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
return function computation() { | ||
if (RunningNode !== null) { | ||
if (node.age === RootClock.time) { | ||
if (node.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
updateNode(node); // checks for state === STALE internally, so don't need to check here | ||
S.on = function on(ev, fn, seed, onchanges) { | ||
if (Array.isArray(ev)) | ||
ev = callAll(ev); | ||
onchanges = !!onchanges; | ||
return S(on, seed); | ||
function on(value) { | ||
var running = RunningNode; | ||
ev(); | ||
if (onchanges) | ||
onchanges = false; | ||
else { | ||
RunningNode = null; | ||
value = fn(value); | ||
RunningNode = running; | ||
} | ||
logComputationRead(node, RunningNode); | ||
return value; | ||
} | ||
return node.value; | ||
}; | ||
}; | ||
// compatibility with commonjs systems that expect default export to be at require('s.js').default rather than just require('s-js') | ||
Object.defineProperty(S, 'default', { value: S }); | ||
S.root = function root(fn) { | ||
var owner = Owner, root = fn.length === 0 ? UNOWNED : new ComputationNode(null, null), result = undefined, disposer = fn.length === 0 ? null : function _dispose() { | ||
function callAll(ss) { | ||
return function all() { | ||
for (var i = 0; i < ss.length; i++) | ||
ss[i](); | ||
}; | ||
} | ||
S.effect = function effect(fn, value) { | ||
new ComputationNode(fn, value); | ||
}; | ||
S.data = function data(value) { | ||
var node = new DataNode(value); | ||
return function data(value) { | ||
if (arguments.length === 0) { | ||
return node.current(); | ||
} | ||
else { | ||
return node.next(value); | ||
} | ||
}; | ||
}; | ||
S.value = function value(current, eq) { | ||
var data = S.data(current), age = -1; | ||
return function value(update) { | ||
if (arguments.length === 0) { | ||
return data(); | ||
} | ||
else { | ||
var same = eq ? eq(current, update) : current === update; | ||
if (!same) { | ||
var time = RootClock.time; | ||
if (age === time) | ||
throw new Error("conflicting values: " + update + " is not the same as " + current); | ||
age = time; | ||
current = update; | ||
data(update); | ||
} | ||
return update; | ||
} | ||
}; | ||
}; | ||
S.freeze = function freeze(fn) { | ||
var result = undefined; | ||
if (RunningClock !== null) { | ||
RootClock.disposes.add(root); | ||
result = fn(); | ||
} | ||
else { | ||
dispose(root); | ||
RunningClock = RootClock; | ||
RunningClock.changes.reset(); | ||
try { | ||
result = fn(); | ||
event(); | ||
} | ||
finally { | ||
RunningClock = null; | ||
} | ||
} | ||
return result; | ||
}; | ||
Owner = root; | ||
if (RunningClock === null) { | ||
result = topLevelRoot(fn, disposer, owner); | ||
} | ||
else { | ||
result = disposer === null ? fn() : fn(disposer); | ||
Owner = owner; | ||
} | ||
return result; | ||
}; | ||
function topLevelRoot(fn, disposer, owner) { | ||
try { | ||
return disposer === null ? fn() : fn(disposer); | ||
} | ||
finally { | ||
Owner = owner; | ||
} | ||
} | ||
S.on = function on(ev, fn, seed, onchanges) { | ||
if (Array.isArray(ev)) | ||
ev = callAll(ev); | ||
onchanges = !!onchanges; | ||
return S(on, seed); | ||
function on(value) { | ||
var running = RunningNode; | ||
ev(); | ||
if (onchanges) | ||
onchanges = false; | ||
else { | ||
S.sample = function sample(fn) { | ||
var result, running = RunningNode; | ||
if (running !== null) { | ||
RunningNode = null; | ||
value = fn(value); | ||
result = fn(); | ||
RunningNode = running; | ||
} | ||
return value; | ||
} | ||
}; | ||
function callAll(ss) { | ||
return function all() { | ||
for (var i = 0; i < ss.length; i++) | ||
ss[i](); | ||
else { | ||
result = fn(); | ||
} | ||
return result; | ||
}; | ||
} | ||
S.data = function data(value) { | ||
var node = new DataNode(value); | ||
return function data(value) { | ||
if (arguments.length > 0) { | ||
S.cleanup = function cleanup(fn) { | ||
if (Owner !== null) { | ||
if (Owner.cleanups === null) | ||
Owner.cleanups = [fn]; | ||
else | ||
Owner.cleanups.push(fn); | ||
} | ||
else { | ||
console.warn("cleanups created without a root or parent will never be run"); | ||
} | ||
}; | ||
// experimental : exposing node constructors and some state | ||
S.makeDataNode = function makeDataNode(value) { | ||
return new DataNode(value); | ||
}; | ||
S.makeComputationNode = function makeComputationNode(fn, seed) { | ||
return new ComputationNode(fn, seed); | ||
}; | ||
S.isFrozen = function isFrozen() { | ||
return RunningClock !== null; | ||
}; | ||
S.isListening = function isListening() { | ||
return RunningNode !== null; | ||
}; | ||
// Internal implementation | ||
/// Graph classes and operations | ||
var Clock = /** @class */ (function () { | ||
function Clock() { | ||
this.time = 0; | ||
this.changes = new Queue(); // batched changes to data nodes | ||
this.updates = new Queue(); // computations to update | ||
this.disposes = new Queue(); // disposals to run after current batch of updates finishes | ||
} | ||
return Clock; | ||
}()); | ||
var RootClockProxy = { | ||
time: function () { return RootClock.time; } | ||
}; | ||
var DataNode = /** @class */ (function () { | ||
function DataNode(value) { | ||
this.value = value; | ||
this.pending = NOTPENDING; | ||
this.log = null; | ||
} | ||
DataNode.prototype.current = function () { | ||
if (RunningNode !== null) { | ||
logDataRead(this, RunningNode); | ||
} | ||
return this.value; | ||
}; | ||
DataNode.prototype.next = function (value) { | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { | ||
if (value !== node.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
if (this.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== this.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + this.pending); | ||
} | ||
} | ||
else { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
else { // add to list of changes | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
} | ||
} | ||
else { | ||
if (node.log !== null) { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
else { // not batching, respond to change now | ||
if (this.log !== null) { | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
event(); | ||
} | ||
else { | ||
node.value = value; | ||
this.value = value; | ||
} | ||
} | ||
return value; | ||
}; | ||
DataNode.prototype.clock = function () { | ||
return RootClockProxy; | ||
}; | ||
return DataNode; | ||
}()); | ||
var ComputationNode = /** @class */ (function () { | ||
function ComputationNode(fn, value) { | ||
this.state = CURRENT; | ||
this.source1 = null; | ||
this.source1slot = 0; | ||
this.sources = null; | ||
this.sourceslots = null; | ||
this.log = null; | ||
this.owned = null; | ||
this.cleanups = null; | ||
this.fn = fn; | ||
this.value = value; | ||
this.age = RootClock.time; | ||
if (fn === null) | ||
return; | ||
var owner = Owner, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
Owner = RunningNode = this; | ||
if (RunningClock === null) { | ||
toplevelComputation(this); | ||
} | ||
else { | ||
this.value = this.fn(this.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [this]; | ||
else | ||
owner.owned.push(this); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
} | ||
else { | ||
ComputationNode.prototype.current = function () { | ||
if (RunningNode !== null) { | ||
logDataRead(node, RunningNode); | ||
if (this.age === RootClock.time) { | ||
if (this.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
updateNode(this); // checks for state === STALE internally, so don't need to check here | ||
} | ||
logComputationRead(this, RunningNode); | ||
} | ||
return node.value; | ||
return this.value; | ||
}; | ||
ComputationNode.prototype.clock = function () { | ||
return RootClockProxy; | ||
}; | ||
return ComputationNode; | ||
}()); | ||
var Log = /** @class */ (function () { | ||
function Log() { | ||
this.node1 = null; | ||
this.node1slot = 0; | ||
this.nodes = null; | ||
this.nodeslots = null; | ||
} | ||
}; | ||
}; | ||
S.value = function value(current, eq) { | ||
var data = S.data(current), age = -1; | ||
return function value(update) { | ||
if (arguments.length === 0) { | ||
return data(); | ||
return Log; | ||
}()); | ||
var Queue = /** @class */ (function () { | ||
function Queue() { | ||
this.items = []; | ||
this.count = 0; | ||
} | ||
else { | ||
var same = eq ? eq(current, update) : current === update; | ||
if (!same) { | ||
var time = RootClock.time; | ||
if (age === time) | ||
throw new Error("conflicting values: " + update + " is not the same as " + current); | ||
age = time; | ||
current = update; | ||
data(update); | ||
Queue.prototype.reset = function () { | ||
this.count = 0; | ||
}; | ||
Queue.prototype.add = function (item) { | ||
this.items[this.count++] = item; | ||
}; | ||
Queue.prototype.run = function (fn) { | ||
var items = this.items; | ||
for (var i = 0; i < this.count; i++) { | ||
fn(items[i]); | ||
items[i] = null; | ||
} | ||
return update; | ||
this.count = 0; | ||
}; | ||
return Queue; | ||
}()); | ||
// Constants | ||
var NOTPENDING = {}, CURRENT = 0, STALE = 1, RUNNING = 2; | ||
// "Globals" used to keep track of current system state | ||
var RootClock = new Clock(), RunningClock = null, // currently running clock | ||
RunningNode = null, // currently running computation | ||
Owner = null, // owner for new computations | ||
UNOWNED = new ComputationNode(null, null); | ||
// Functions | ||
function logRead(from, to) { | ||
var fromslot, toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; | ||
if (from.node1 === null) { | ||
from.node1 = to; | ||
from.node1slot = toslot; | ||
fromslot = -1; | ||
} | ||
}; | ||
}; | ||
S.freeze = function freeze(fn) { | ||
var result = undefined; | ||
if (RunningClock !== null) { | ||
result = fn(); | ||
} | ||
else { | ||
RunningClock = RootClock; | ||
RunningClock.changes.reset(); | ||
try { | ||
result = fn(); | ||
event(); | ||
else if (from.nodes === null) { | ||
from.nodes = [to]; | ||
from.nodeslots = [toslot]; | ||
fromslot = 0; | ||
} | ||
finally { | ||
RunningClock = null; | ||
else { | ||
fromslot = from.nodes.length; | ||
from.nodes.push(to); | ||
from.nodeslots.push(toslot); | ||
} | ||
} | ||
return result; | ||
}; | ||
S.sample = function sample(fn) { | ||
var result, running = RunningNode; | ||
if (running !== null) { | ||
RunningNode = null; | ||
result = fn(); | ||
RunningNode = running; | ||
} | ||
else { | ||
result = fn(); | ||
} | ||
return result; | ||
}; | ||
S.cleanup = function cleanup(fn) { | ||
if (Owner !== null) { | ||
if (Owner.cleanups === null) | ||
Owner.cleanups = [fn]; | ||
else | ||
Owner.cleanups.push(fn); | ||
} | ||
else { | ||
console.warn("cleanups created without a root or parent will never be run"); | ||
} | ||
}; | ||
// Internal implementation | ||
/// Graph classes and operations | ||
var Clock = /** @class */ (function () { | ||
function Clock() { | ||
this.time = 0; | ||
this.changes = new Queue(); // batched changes to data nodes | ||
this.updates = new Queue(); // computations to update | ||
this.disposes = new Queue(); // disposals to run after current batch of updates finishes | ||
} | ||
return Clock; | ||
}()); | ||
var DataNode = /** @class */ (function () { | ||
function DataNode(value) { | ||
this.value = value; | ||
this.pending = NOTPENDING; | ||
this.log = null; | ||
} | ||
return DataNode; | ||
}()); | ||
var ComputationNode = /** @class */ (function () { | ||
function ComputationNode(fn, value) { | ||
this.fn = fn; | ||
this.value = value; | ||
this.state = CURRENT; | ||
this.source1 = null; | ||
this.source1slot = 0; | ||
this.sources = null; | ||
this.sourceslots = null; | ||
this.log = null; | ||
this.owned = null; | ||
this.cleanups = null; | ||
this.age = RootClock.time; | ||
} | ||
return ComputationNode; | ||
}()); | ||
var Log = /** @class */ (function () { | ||
function Log() { | ||
this.node1 = null; | ||
this.node1slot = 0; | ||
this.nodes = null; | ||
this.nodeslots = null; | ||
} | ||
return Log; | ||
}()); | ||
var Queue = /** @class */ (function () { | ||
function Queue() { | ||
this.items = []; | ||
this.count = 0; | ||
} | ||
Queue.prototype.reset = function () { | ||
this.count = 0; | ||
}; | ||
Queue.prototype.add = function (item) { | ||
this.items[this.count++] = item; | ||
}; | ||
Queue.prototype.run = function (fn) { | ||
var items = this.items; | ||
for (var i = 0; i < this.count; i++) { | ||
fn(items[i]); | ||
items[i] = null; | ||
if (to.source1 === null) { | ||
to.source1 = from; | ||
to.source1slot = fromslot; | ||
} | ||
this.count = 0; | ||
}; | ||
return Queue; | ||
}()); | ||
// Constants | ||
var NOTPENDING = {}; | ||
var CURRENT = 0; | ||
var STALE = 1; | ||
var RUNNING = 2; | ||
// "Globals" used to keep track of current system state | ||
var RootClock = new Clock(); | ||
var RunningClock = null; | ||
var RunningNode = null; | ||
var Owner = null; | ||
var UNOWNED = new ComputationNode(null, null); | ||
// Functions | ||
function logRead(from, to) { | ||
var fromslot, toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; | ||
if (from.node1 === null) { | ||
from.node1 = to; | ||
from.node1slot = toslot; | ||
fromslot = -1; | ||
else if (to.sources === null) { | ||
to.sources = [from]; | ||
to.sourceslots = [fromslot]; | ||
} | ||
else { | ||
to.sources.push(from); | ||
to.sourceslots.push(fromslot); | ||
} | ||
} | ||
else if (from.nodes === null) { | ||
from.nodes = [to]; | ||
from.nodeslots = [toslot]; | ||
fromslot = 0; | ||
function logDataRead(data, to) { | ||
if (data.log === null) | ||
data.log = new Log(); | ||
logRead(data.log, to); | ||
} | ||
else { | ||
fromslot = from.nodes.length; | ||
from.nodes.push(to); | ||
from.nodeslots.push(toslot); | ||
function logComputationRead(node, to) { | ||
if (node.log === null) | ||
node.log = new Log(); | ||
logRead(node.log, to); | ||
} | ||
if (to.source1 === null) { | ||
to.source1 = from; | ||
to.source1slot = fromslot; | ||
} | ||
else if (to.sources === null) { | ||
to.sources = [from]; | ||
to.sourceslots = [fromslot]; | ||
} | ||
else { | ||
to.sources.push(from); | ||
to.sourceslots.push(fromslot); | ||
} | ||
} | ||
function logDataRead(data, to) { | ||
if (data.log === null) | ||
data.log = new Log(); | ||
logRead(data.log, to); | ||
} | ||
function logComputationRead(node, to) { | ||
if (node.log === null) | ||
node.log = new Log(); | ||
logRead(node.log, to); | ||
} | ||
function event() { | ||
// b/c we might be under a top level S.root(), have to preserve current root | ||
var owner = Owner; | ||
RootClock.updates.reset(); | ||
RootClock.time++; | ||
try { | ||
run(RootClock); | ||
} | ||
finally { | ||
RunningClock = RunningNode = null; | ||
Owner = owner; | ||
} | ||
} | ||
function toplevelComputation(node) { | ||
RunningClock = RootClock; | ||
RootClock.changes.reset(); | ||
RootClock.updates.reset(); | ||
try { | ||
node.value = node.fn(node.value); | ||
if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { | ||
RootClock.time++; | ||
function event() { | ||
// b/c we might be under a top level S.root(), have to preserve current root | ||
var owner = Owner; | ||
RootClock.updates.reset(); | ||
RootClock.time++; | ||
try { | ||
run(RootClock); | ||
} | ||
finally { | ||
RunningClock = RunningNode = null; | ||
Owner = owner; | ||
} | ||
} | ||
finally { | ||
RunningClock = Owner = RunningNode = null; | ||
} | ||
} | ||
function run(clock) { | ||
var running = RunningClock, count = 0; | ||
RunningClock = clock; | ||
clock.disposes.reset(); | ||
// for each batch ... | ||
while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) | ||
clock.time++; | ||
clock.changes.run(applyDataChange); | ||
clock.updates.run(updateNode); | ||
clock.disposes.run(dispose); | ||
// if there are still changes after excessive batches, assume runaway | ||
if (count++ > 1e5) { | ||
throw new Error("Runaway clock detected"); | ||
function toplevelComputation(node) { | ||
RunningClock = RootClock; | ||
RootClock.changes.reset(); | ||
RootClock.updates.reset(); | ||
try { | ||
node.value = node.fn(node.value); | ||
if (RootClock.changes.count > 0 || RootClock.updates.count > 0) { | ||
RootClock.time++; | ||
run(RootClock); | ||
} | ||
} | ||
finally { | ||
RunningClock = Owner = RunningNode = null; | ||
} | ||
} | ||
RunningClock = running; | ||
} | ||
function applyDataChange(data) { | ||
data.value = data.pending; | ||
data.pending = NOTPENDING; | ||
if (data.log) | ||
markComputationsStale(data.log); | ||
} | ||
function markComputationsStale(log) { | ||
var node1 = log.node1, nodes = log.nodes; | ||
// mark all downstream nodes stale which haven't been already | ||
if (node1 !== null) | ||
markNodeStale(node1); | ||
if (nodes !== null) { | ||
for (var i = 0, len = nodes.length; i < len; i++) { | ||
markNodeStale(nodes[i]); | ||
function run(clock) { | ||
var running = RunningClock, count = 0; | ||
RunningClock = clock; | ||
clock.disposes.reset(); | ||
// for each batch ... | ||
while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) // don't tick on first run, or else we expire already scheduled updates | ||
clock.time++; | ||
clock.changes.run(applyDataChange); | ||
clock.updates.run(updateNode); | ||
clock.disposes.run(dispose); | ||
// if there are still changes after excessive batches, assume runaway | ||
if (count++ > 1e5) { | ||
throw new Error("Runaway clock detected"); | ||
} | ||
} | ||
RunningClock = running; | ||
} | ||
} | ||
function markNodeStale(node) { | ||
var time = RootClock.time; | ||
if (node.age < time) { | ||
node.age = time; | ||
node.state = STALE; | ||
RootClock.updates.add(node); | ||
if (node.owned !== null) | ||
markOwnedNodesForDisposal(node.owned); | ||
if (node.log !== null) | ||
markComputationsStale(node.log); | ||
function applyDataChange(data) { | ||
data.value = data.pending; | ||
data.pending = NOTPENDING; | ||
if (data.log) | ||
markComputationsStale(data.log); | ||
} | ||
} | ||
function markOwnedNodesForDisposal(owned) { | ||
for (var i = 0; i < owned.length; i++) { | ||
var child = owned[i]; | ||
child.age = RootClock.time; | ||
child.state = CURRENT; | ||
if (child.owned !== null) | ||
markOwnedNodesForDisposal(child.owned); | ||
function markComputationsStale(log) { | ||
var node1 = log.node1, nodes = log.nodes; | ||
// mark all downstream nodes stale which haven't been already | ||
if (node1 !== null) | ||
markNodeStale(node1); | ||
if (nodes !== null) { | ||
for (var i = 0, len = nodes.length; i < len; i++) { | ||
markNodeStale(nodes[i]); | ||
} | ||
} | ||
} | ||
} | ||
function updateNode(node) { | ||
if (node.state === STALE) { | ||
var owner = Owner, running = RunningNode; | ||
Owner = RunningNode = node; | ||
node.state = RUNNING; | ||
cleanup(node, false); | ||
node.value = node.fn(node.value); | ||
node.state = CURRENT; | ||
Owner = owner; | ||
RunningNode = running; | ||
} | ||
} | ||
function cleanup(node, final) { | ||
var source1 = node.source1, sources = node.sources, sourceslots = node.sourceslots, cleanups = node.cleanups, owned = node.owned, i, len; | ||
if (cleanups !== null) { | ||
for (i = 0; i < cleanups.length; i++) { | ||
cleanups[i](final); | ||
function markNodeStale(node) { | ||
var time = RootClock.time; | ||
if (node.age < time) { | ||
node.age = time; | ||
node.state = STALE; | ||
RootClock.updates.add(node); | ||
if (node.owned !== null) | ||
markOwnedNodesForDisposal(node.owned); | ||
if (node.log !== null) | ||
markComputationsStale(node.log); | ||
} | ||
node.cleanups = null; | ||
} | ||
if (owned !== null) { | ||
for (i = 0; i < owned.length; i++) { | ||
dispose(owned[i]); | ||
function markOwnedNodesForDisposal(owned) { | ||
for (var i = 0; i < owned.length; i++) { | ||
var child = owned[i]; | ||
child.age = RootClock.time; | ||
child.state = CURRENT; | ||
if (child.owned !== null) | ||
markOwnedNodesForDisposal(child.owned); | ||
} | ||
node.owned = null; | ||
} | ||
if (source1 !== null) { | ||
cleanupSource(source1, node.source1slot); | ||
node.source1 = null; | ||
} | ||
if (sources !== null) { | ||
for (i = 0, len = sources.length; i < len; i++) { | ||
cleanupSource(sources.pop(), sourceslots.pop()); | ||
function updateNode(node) { | ||
if (node.state === STALE) { | ||
var owner = Owner, running = RunningNode; | ||
Owner = RunningNode = node; | ||
node.state = RUNNING; | ||
cleanup(node, false); | ||
node.value = node.fn(node.value); | ||
node.state = CURRENT; | ||
Owner = owner; | ||
RunningNode = running; | ||
} | ||
} | ||
} | ||
function cleanupSource(source, slot) { | ||
var nodes = source.nodes, nodeslots = source.nodeslots, last, lastslot; | ||
if (slot === -1) { | ||
source.node1 = null; | ||
} | ||
else { | ||
last = nodes.pop(); | ||
lastslot = nodeslots.pop(); | ||
if (slot !== nodes.length) { | ||
nodes[slot] = last; | ||
nodeslots[slot] = lastslot; | ||
if (lastslot === -1) { | ||
last.source1slot = slot; | ||
function cleanup(node, final) { | ||
var source1 = node.source1, sources = node.sources, sourceslots = node.sourceslots, cleanups = node.cleanups, owned = node.owned, i, len; | ||
if (cleanups !== null) { | ||
for (i = 0; i < cleanups.length; i++) { | ||
cleanups[i](final); | ||
} | ||
else { | ||
last.sourceslots[lastslot] = slot; | ||
node.cleanups = null; | ||
} | ||
if (owned !== null) { | ||
for (i = 0; i < owned.length; i++) { | ||
dispose(owned[i]); | ||
} | ||
node.owned = null; | ||
} | ||
if (source1 !== null) { | ||
cleanupSource(source1, node.source1slot); | ||
node.source1 = null; | ||
} | ||
if (sources !== null) { | ||
for (i = 0, len = sources.length; i < len; i++) { | ||
cleanupSource(sources.pop(), sourceslots.pop()); | ||
} | ||
} | ||
} | ||
} | ||
function dispose(node) { | ||
node.fn = null; | ||
node.log = null; | ||
cleanup(node, true); | ||
} | ||
function cleanupSource(source, slot) { | ||
var nodes = source.nodes, nodeslots = source.nodeslots, last, lastslot; | ||
if (slot === -1) { | ||
source.node1 = null; | ||
} | ||
else { | ||
last = nodes.pop(); | ||
lastslot = nodeslots.pop(); | ||
if (slot !== nodes.length) { | ||
nodes[slot] = last; | ||
nodeslots[slot] = lastslot; | ||
if (lastslot === -1) { | ||
last.source1slot = slot; | ||
} | ||
else { | ||
last.sourceslots[lastslot] = slot; | ||
} | ||
} | ||
} | ||
} | ||
function dispose(node) { | ||
node.fn = null; | ||
node.log = null; | ||
cleanup(node, true); | ||
} | ||
return S; | ||
return S; | ||
}))); |
(function (global, factory) { | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.S = factory()); | ||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : | ||
typeof define === 'function' && define.amd ? define(factory) : | ||
(global.S = factory()); | ||
}(this, (function () { 'use strict'; | ||
// Public interface | ||
var S = function S(fn, value) { | ||
var owner = Owner, clock = RunningClock === null ? RootClock : RunningClock, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
var node = new ComputationNode(clock, fn, value); | ||
Owner = RunningNode = node; | ||
if (RunningClock === null) { | ||
toplevelComputation(node); | ||
} | ||
else { | ||
node.value = node.fn(node.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [node]; | ||
else | ||
owner.owned.push(node); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
return function computation() { | ||
if (RunningNode !== null) { | ||
var rclock = RunningClock, sclock = node.clock; | ||
while (rclock.depth > sclock.depth + 1) | ||
rclock = rclock.parent; | ||
if (rclock === sclock || rclock.parent === sclock) { | ||
if (node.preclocks !== null) { | ||
for (var i = 0; i < node.preclocks.count; i++) { | ||
var preclock = node.preclocks.clocks[i]; | ||
updateClock(preclock); | ||
// Public interface | ||
var S = function S(fn, value) { | ||
var owner = Owner, clock = RunningClock === null ? RootClock : RunningClock, running = RunningNode; | ||
if (owner === null) | ||
console.warn("computations created without a root or parent will never be disposed"); | ||
var node = new ComputationNode(clock, fn, value); | ||
Owner = RunningNode = node; | ||
if (RunningClock === null) { | ||
toplevelComputation(node); | ||
} | ||
else { | ||
node.value = node.fn(node.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) | ||
owner.owned = [node]; | ||
else | ||
owner.owned.push(node); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
return function computation() { | ||
if (RunningNode !== null) { | ||
var rclock = RunningClock, sclock = node.clock; | ||
while (rclock.depth > sclock.depth + 1) | ||
rclock = rclock.parent; | ||
if (rclock === sclock || rclock.parent === sclock) { | ||
if (node.preclocks !== null) { | ||
for (var i = 0; i < node.preclocks.count; i++) { | ||
var preclock = node.preclocks.clocks[i]; | ||
updateClock(preclock); | ||
} | ||
} | ||
} | ||
if (node.age === node.clock.time()) { | ||
if (node.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
updateNode(node); // checks for state === STALE internally, so don't need to check here | ||
} | ||
if (node.preclocks !== null) { | ||
for (var i = 0; i < node.preclocks.count; i++) { | ||
var preclock = node.preclocks.clocks[i]; | ||
if (rclock === sclock) | ||
logNodePreClock(preclock, RunningNode); | ||
if (node.age === node.clock.time()) { | ||
if (node.state === RUNNING) | ||
throw new Error("circular dependency"); | ||
else | ||
logClockPreClock(preclock, rclock, RunningNode); | ||
updateNode(node); // checks for state === STALE internally, so don't need to check here | ||
} | ||
if (node.preclocks !== null) { | ||
for (var i = 0; i < node.preclocks.count; i++) { | ||
var preclock = node.preclocks.clocks[i]; | ||
if (rclock === sclock) | ||
logNodePreClock(preclock, RunningNode); | ||
else | ||
logClockPreClock(preclock, rclock, RunningNode); | ||
} | ||
} | ||
} | ||
} | ||
else { | ||
if (rclock.depth > sclock.depth) | ||
rclock = rclock.parent; | ||
while (sclock.depth > rclock.depth + 1) | ||
sclock = sclock.parent; | ||
if (sclock.parent === rclock) { | ||
logNodePreClock(sclock, RunningNode); | ||
} | ||
else { | ||
if (sclock.depth > rclock.depth) | ||
if (rclock.depth > sclock.depth) | ||
rclock = rclock.parent; | ||
while (sclock.depth > rclock.depth + 1) | ||
sclock = sclock.parent; | ||
while (rclock.parent !== sclock.parent) | ||
rclock = rclock.parent, sclock = sclock.parent; | ||
logClockPreClock(sclock, rclock, RunningNode); | ||
if (sclock.parent === rclock) { | ||
logNodePreClock(sclock, RunningNode); | ||
} | ||
else { | ||
if (sclock.depth > rclock.depth) | ||
sclock = sclock.parent; | ||
while (rclock.parent !== sclock.parent) | ||
rclock = rclock.parent, sclock = sclock.parent; | ||
logClockPreClock(sclock, rclock, RunningNode); | ||
} | ||
updateClock(sclock); | ||
} | ||
updateClock(sclock); | ||
logComputationRead(node, RunningNode); | ||
} | ||
logComputationRead(node, RunningNode); | ||
} | ||
return node.value; | ||
return node.value; | ||
}; | ||
}; | ||
}; | ||
// compatibility with commonjs systems that expect default export to be at require('s.js').default rather than just require('s-js') | ||
Object.defineProperty(S, 'default', { value: S }); | ||
S.root = function root(fn) { | ||
var owner = Owner, root = fn.length === 0 ? UNOWNED : new ComputationNode(RunningClock || RootClock, null, null), result = undefined, disposer = fn.length === 0 ? null : function _dispose() { | ||
if (RunningClock !== null) { | ||
markClockStale(root.clock); | ||
root.clock.disposes.add(root); | ||
// compatibility with commonjs systems that expect default export to be at require('s.js').default rather than just require('s-js') | ||
Object.defineProperty(S, 'default', { value: S }); | ||
S.root = function root(fn) { | ||
var owner = Owner, root = fn.length === 0 ? UNOWNED : new ComputationNode(RunningClock || RootClock, null, null), result = undefined, disposer = fn.length === 0 ? null : function _dispose() { | ||
if (RunningClock !== null) { | ||
markClockStale(root.clock); | ||
root.clock.disposes.add(root); | ||
} | ||
else { | ||
dispose(root); | ||
} | ||
}; | ||
Owner = root; | ||
if (RunningClock === null) { | ||
result = topLevelRoot(fn, disposer, owner); | ||
} | ||
else { | ||
dispose(root); | ||
result = disposer === null ? fn() : fn(disposer); | ||
Owner = owner; | ||
} | ||
return result; | ||
}; | ||
Owner = root; | ||
if (RunningClock === null) { | ||
result = topLevelRoot(fn, disposer, owner); | ||
} | ||
else { | ||
result = disposer === null ? fn() : fn(disposer); | ||
Owner = owner; | ||
} | ||
return result; | ||
}; | ||
function topLevelRoot(fn, disposer, owner) { | ||
try { | ||
return disposer === null ? fn() : fn(disposer); | ||
} | ||
finally { | ||
Owner = owner; | ||
} | ||
} | ||
S.on = function on(ev, fn, seed, onchanges) { | ||
if (Array.isArray(ev)) | ||
ev = callAll(ev); | ||
onchanges = !!onchanges; | ||
return S(on, seed); | ||
function on(value) { | ||
var running = RunningNode; | ||
ev(); | ||
if (onchanges) | ||
onchanges = false; | ||
else { | ||
RunningNode = null; | ||
value = fn(value); | ||
RunningNode = running; | ||
function topLevelRoot(fn, disposer, owner) { | ||
try { | ||
return disposer === null ? fn() : fn(disposer); | ||
} | ||
return value; | ||
finally { | ||
Owner = owner; | ||
} | ||
} | ||
}; | ||
function callAll(ss) { | ||
return function all() { | ||
for (var i = 0; i < ss.length; i++) | ||
ss[i](); | ||
}; | ||
} | ||
S.data = function data(value) { | ||
var node = new DataNode(RunningClock === null ? RootClock : RunningClock, value); | ||
return function data(value) { | ||
var rclock = RunningClock, sclock = node.clock; | ||
if (RunningClock !== null) { | ||
while (rclock.depth > sclock.depth) | ||
rclock = rclock.parent; | ||
while (sclock.depth > rclock.depth && sclock.parent !== rclock) | ||
sclock = sclock.parent; | ||
if (sclock.parent !== rclock) | ||
while (rclock.parent !== sclock.parent) | ||
rclock = rclock.parent, sclock = sclock.parent; | ||
if (rclock !== sclock) { | ||
updateClock(sclock); | ||
S.on = function on(ev, fn, seed, onchanges) { | ||
if (Array.isArray(ev)) | ||
ev = callAll(ev); | ||
onchanges = !!onchanges; | ||
return S(on, seed); | ||
function on(value) { | ||
var running = RunningNode; | ||
ev(); | ||
if (onchanges) | ||
onchanges = false; | ||
else { | ||
RunningNode = null; | ||
value = fn(value); | ||
RunningNode = running; | ||
} | ||
return value; | ||
} | ||
var cclock = rclock === sclock ? sclock : sclock.parent; | ||
if (arguments.length > 0) { | ||
}; | ||
function callAll(ss) { | ||
return function all() { | ||
for (var i = 0; i < ss.length; i++) | ||
ss[i](); | ||
}; | ||
} | ||
S.data = function data(value) { | ||
var node = new DataNode(RunningClock === null ? RootClock : RunningClock, value); | ||
return function data(value) { | ||
var rclock = RunningClock, sclock = node.clock; | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { | ||
if (value !== node.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
while (rclock.depth > sclock.depth) | ||
rclock = rclock.parent; | ||
while (sclock.depth > rclock.depth && sclock.parent !== rclock) | ||
sclock = sclock.parent; | ||
if (sclock.parent !== rclock) | ||
while (rclock.parent !== sclock.parent) | ||
rclock = rclock.parent, sclock = sclock.parent; | ||
if (rclock !== sclock) { | ||
updateClock(sclock); | ||
} | ||
} | ||
var cclock = rclock === sclock ? sclock : sclock.parent; | ||
if (arguments.length > 0) { | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== node.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
} | ||
} | ||
else { // add to list of changes | ||
markClockStale(cclock); | ||
node.pending = value; | ||
cclock.changes.add(node); | ||
} | ||
} | ||
else { | ||
markClockStale(cclock); | ||
node.pending = value; | ||
cclock.changes.add(node); | ||
else { // not batching, respond to change now | ||
if (node.log !== null) { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
event(); | ||
} | ||
else { | ||
node.value = value; | ||
} | ||
} | ||
return value; | ||
} | ||
else { | ||
if (node.log !== null) { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
event(); | ||
if (RunningNode !== null) { | ||
logDataRead(node, RunningNode); | ||
if (sclock.parent === rclock) | ||
logNodePreClock(sclock, RunningNode); | ||
else if (sclock !== rclock) | ||
logClockPreClock(sclock, rclock, RunningNode); | ||
} | ||
else { | ||
node.value = value; | ||
return node.value; | ||
} | ||
}; | ||
}; | ||
S.value = function value(current, eq) { | ||
var data = S.data(current), clock = RunningClock || RootClock, age = -1; | ||
return function value(update) { | ||
if (arguments.length === 0) { | ||
return data(); | ||
} | ||
else { | ||
var same = eq ? eq(current, update) : current === update; | ||
if (!same) { | ||
var time = clock.time(); | ||
if (age === time) | ||
throw new Error("conflicting values: " + update + " is not the same as " + current); | ||
age = time; | ||
current = update; | ||
data(update); | ||
} | ||
return update; | ||
} | ||
return value; | ||
}; | ||
}; | ||
S.freeze = function freeze(fn) { | ||
var result = undefined; | ||
if (RunningClock !== null) { | ||
result = fn(); | ||
} | ||
else { | ||
if (RunningNode !== null) { | ||
logDataRead(node, RunningNode); | ||
if (sclock.parent === rclock) | ||
logNodePreClock(sclock, RunningNode); | ||
else if (sclock !== rclock) | ||
logClockPreClock(sclock, rclock, RunningNode); | ||
RunningClock = RootClock; | ||
RunningClock.changes.reset(); | ||
try { | ||
result = fn(); | ||
event(); | ||
} | ||
return node.value; | ||
finally { | ||
RunningClock = null; | ||
} | ||
} | ||
return result; | ||
}; | ||
}; | ||
S.value = function value(current, eq) { | ||
var data = S.data(current), clock = RunningClock || RootClock, age = -1; | ||
return function value(update) { | ||
if (arguments.length === 0) { | ||
return data(); | ||
S.sample = function sample(fn) { | ||
var result, running = RunningNode; | ||
if (running !== null) { | ||
RunningNode = null; | ||
result = fn(); | ||
RunningNode = running; | ||
} | ||
else { | ||
var same = eq ? eq(current, update) : current === update; | ||
if (!same) { | ||
var time = clock.time(); | ||
if (age === time) | ||
throw new Error("conflicting values: " + update + " is not the same as " + current); | ||
age = time; | ||
current = update; | ||
data(update); | ||
result = fn(); | ||
} | ||
return result; | ||
}; | ||
S.cleanup = function cleanup(fn) { | ||
if (Owner !== null) { | ||
if (Owner.cleanups === null) | ||
Owner.cleanups = [fn]; | ||
else | ||
Owner.cleanups.push(fn); | ||
} | ||
else { | ||
console.warn("cleanups created without a root or parent will never be run"); | ||
} | ||
}; | ||
S.subclock = function subclock(fn) { | ||
var clock = new Clock(RunningClock || RootClock); | ||
return fn === undefined ? subclock : subclock(fn); | ||
function subclock(fn) { | ||
var result = null, running = RunningClock; | ||
RunningClock = clock; | ||
clock.state = STALE; | ||
try { | ||
result = fn(); | ||
clock.subtime++; | ||
run(clock); | ||
} | ||
return update; | ||
finally { | ||
RunningClock = running; | ||
} | ||
// if we were run from top level, have to flush any changes in RootClock | ||
if (RunningClock === null) | ||
event(); | ||
return result; | ||
} | ||
}; | ||
}; | ||
S.freeze = function freeze(fn) { | ||
var result = undefined; | ||
if (RunningClock !== null) { | ||
result = fn(); | ||
} | ||
else { | ||
RunningClock = RootClock; | ||
RunningClock.changes.reset(); | ||
try { | ||
result = fn(); | ||
event(); | ||
// Internal implementation | ||
/// Graph classes and operations | ||
var Clock = /** @class */ (function () { | ||
function Clock(parent) { | ||
this.parent = parent; | ||
this.id = Clock.count++; | ||
this.state = CURRENT; | ||
this.subtime = 0; | ||
this.preclocks = null; | ||
this.changes = new Queue(); // batched changes to data nodes | ||
this.subclocks = new Queue(); // subclocks that need to be updated | ||
this.updates = new Queue(); // computations to update | ||
this.disposes = new Queue(); // disposals to run after current batch of updates finishes | ||
if (parent !== null) { | ||
this.age = parent.time(); | ||
this.depth = parent.depth + 1; | ||
} | ||
else { | ||
this.age = 0; | ||
this.depth = 0; | ||
} | ||
} | ||
finally { | ||
RunningClock = null; | ||
Clock.prototype.time = function () { | ||
var time = this.subtime, p = this; | ||
while ((p = p.parent) !== null) | ||
time += p.subtime; | ||
return time; | ||
}; | ||
Clock.count = 0; | ||
return Clock; | ||
}()); | ||
var DataNode = /** @class */ (function () { | ||
function DataNode(clock, value) { | ||
this.clock = clock; | ||
this.value = value; | ||
this.pending = NOTPENDING; | ||
this.log = null; | ||
} | ||
return DataNode; | ||
}()); | ||
var ComputationNode = /** @class */ (function () { | ||
function ComputationNode(clock, fn, value) { | ||
this.clock = clock; | ||
this.fn = fn; | ||
this.value = value; | ||
this.state = CURRENT; | ||
this.source1 = null; | ||
this.source1slot = 0; | ||
this.sources = null; | ||
this.sourceslots = null; | ||
this.log = null; | ||
this.preclocks = null; | ||
this.owned = null; | ||
this.cleanups = null; | ||
this.age = this.clock.time(); | ||
} | ||
return ComputationNode; | ||
}()); | ||
var Log = /** @class */ (function () { | ||
function Log() { | ||
this.node1 = null; | ||
this.node1slot = 0; | ||
this.nodes = null; | ||
this.nodeslots = null; | ||
} | ||
return Log; | ||
}()); | ||
var NodePreClockLog = /** @class */ (function () { | ||
function NodePreClockLog() { | ||
this.count = 0; | ||
this.clocks = []; // [clock], where clock.parent === node.clock | ||
this.ages = []; // clock.id -> node.age | ||
this.ucount = 0; // number of ancestor clocks with preclocks from this node | ||
this.uclocks = []; | ||
this.uclockids = []; | ||
} | ||
return NodePreClockLog; | ||
}()); | ||
var ClockPreClockLog = /** @class */ (function () { | ||
function ClockPreClockLog() { | ||
this.count = 0; | ||
this.clockcounts = []; // clock.id -> ref count | ||
this.clocks = []; // clock.id -> clock | ||
this.ids = []; // [clock.id] | ||
} | ||
return ClockPreClockLog; | ||
}()); | ||
var Queue = /** @class */ (function () { | ||
function Queue() { | ||
this.items = []; | ||
this.count = 0; | ||
} | ||
Queue.prototype.reset = function () { | ||
this.count = 0; | ||
}; | ||
Queue.prototype.add = function (item) { | ||
this.items[this.count++] = item; | ||
}; | ||
Queue.prototype.run = function (fn) { | ||
var items = this.items; | ||
for (var i = 0; i < this.count; i++) { | ||
fn(items[i]); | ||
items[i] = null; | ||
} | ||
this.count = 0; | ||
}; | ||
return Queue; | ||
}()); | ||
// Constants | ||
var NOTPENDING = {}, CURRENT = 0, STALE = 1, RUNNING = 2; | ||
// "Globals" used to keep track of current system state | ||
var RootClock = new Clock(null), RunningClock = null, // currently running clock | ||
RunningNode = null, // currently running computation | ||
Owner = null, // owner for new computations | ||
UNOWNED = new ComputationNode(RootClock, null, null); | ||
// Functions | ||
function logRead(from, to) { | ||
var fromslot, toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; | ||
if (from.node1 === null) { | ||
from.node1 = to; | ||
from.node1slot = toslot; | ||
fromslot = -1; | ||
} | ||
else if (from.nodes === null) { | ||
from.nodes = [to]; | ||
from.nodeslots = [toslot]; | ||
fromslot = 0; | ||
} | ||
else { | ||
fromslot = from.nodes.length; | ||
from.nodes.push(to); | ||
from.nodeslots.push(toslot); | ||
} | ||
if (to.source1 === null) { | ||
to.source1 = from; | ||
to.source1slot = fromslot; | ||
} | ||
else if (to.sources === null) { | ||
to.sources = [from]; | ||
to.sourceslots = [fromslot]; | ||
} | ||
else { | ||
to.sources.push(from); | ||
to.sourceslots.push(fromslot); | ||
} | ||
} | ||
return result; | ||
}; | ||
S.sample = function sample(fn) { | ||
var result, running = RunningNode; | ||
if (running !== null) { | ||
RunningNode = null; | ||
result = fn(); | ||
RunningNode = running; | ||
function logDataRead(data, to) { | ||
if (data.log === null) | ||
data.log = new Log(); | ||
logRead(data.log, to); | ||
} | ||
else { | ||
result = fn(); | ||
function logComputationRead(node, to) { | ||
if (node.log === null) | ||
node.log = new Log(); | ||
logRead(node.log, to); | ||
} | ||
return result; | ||
}; | ||
S.cleanup = function cleanup(fn) { | ||
if (Owner !== null) { | ||
if (Owner.cleanups === null) | ||
Owner.cleanups = [fn]; | ||
else | ||
Owner.cleanups.push(fn); | ||
function logNodePreClock(clock, to) { | ||
if (to.preclocks === null) | ||
to.preclocks = new NodePreClockLog(); | ||
else if (to.preclocks.ages[clock.id] === to.age) | ||
return; | ||
to.preclocks.ages[clock.id] = to.age; | ||
to.preclocks.clocks[to.preclocks.count++] = clock; | ||
} | ||
else { | ||
console.warn("cleanups created without a root or parent will never be run"); | ||
function logClockPreClock(sclock, rclock, rnode) { | ||
var clocklog = rclock.preclocks === null ? (rclock.preclocks = new ClockPreClockLog()) : rclock.preclocks, nodelog = rnode.preclocks === null ? (rnode.preclocks = new NodePreClockLog()) : rnode.preclocks; | ||
if (nodelog.ages[sclock.id] === rnode.age) | ||
return; | ||
nodelog.ages[sclock.id] = rnode.age; | ||
nodelog.uclocks[nodelog.ucount] = rclock; | ||
nodelog.uclockids[nodelog.ucount++] = sclock.id; | ||
var clockcount = clocklog.clockcounts[sclock.id]; | ||
if (clockcount === undefined) { | ||
clocklog.ids[clocklog.count++] = sclock.id; | ||
clocklog.clockcounts[sclock.id] = 1; | ||
clocklog.clocks[sclock.id] = sclock; | ||
} | ||
else if (clockcount === 0) { | ||
clocklog.clockcounts[sclock.id] = 1; | ||
clocklog.clocks[sclock.id] = sclock; | ||
} | ||
else { | ||
clocklog.clockcounts[sclock.id]++; | ||
} | ||
} | ||
}; | ||
S.subclock = function subclock(fn) { | ||
var clock = new Clock(RunningClock || RootClock); | ||
return fn === undefined ? subclock : subclock(fn); | ||
function subclock(fn) { | ||
var result = null, running = RunningClock; | ||
RunningClock = clock; | ||
clock.state = STALE; | ||
function event() { | ||
// b/c we might be under a top level S.root(), have to preserve current root | ||
var owner = Owner; | ||
RootClock.subclocks.reset(); | ||
RootClock.updates.reset(); | ||
RootClock.subtime++; | ||
try { | ||
result = fn(); | ||
clock.subtime++; | ||
run(clock); | ||
run(RootClock); | ||
} | ||
finally { | ||
RunningClock = running; | ||
RunningClock = RunningNode = null; | ||
Owner = owner; | ||
} | ||
// if we were run from top level, have to flush any changes in RootClock | ||
if (RunningClock === null) | ||
event(); | ||
return result; | ||
} | ||
}; | ||
// Internal implementation | ||
/// Graph classes and operations | ||
var Clock = /** @class */ (function () { | ||
function Clock(parent) { | ||
this.parent = parent; | ||
this.id = Clock.count++; | ||
this.state = CURRENT; | ||
this.subtime = 0; | ||
this.preclocks = null; | ||
this.changes = new Queue(); // batched changes to data nodes | ||
this.subclocks = new Queue(); // subclocks that need to be updated | ||
this.updates = new Queue(); // computations to update | ||
this.disposes = new Queue(); // disposals to run after current batch of updates finishes | ||
if (parent !== null) { | ||
this.age = parent.time(); | ||
this.depth = parent.depth + 1; | ||
function toplevelComputation(node) { | ||
RunningClock = RootClock; | ||
RootClock.changes.reset(); | ||
RootClock.subclocks.reset(); | ||
RootClock.updates.reset(); | ||
try { | ||
node.value = node.fn(node.value); | ||
if (RootClock.changes.count > 0 || RootClock.subclocks.count > 0 || RootClock.updates.count > 0) { | ||
RootClock.subtime++; | ||
run(RootClock); | ||
} | ||
} | ||
else { | ||
this.age = 0; | ||
this.depth = 0; | ||
finally { | ||
RunningClock = Owner = RunningNode = null; | ||
} | ||
} | ||
Clock.prototype.time = function () { | ||
var time = this.subtime, p = this; | ||
while ((p = p.parent) !== null) | ||
time += p.subtime; | ||
return time; | ||
}; | ||
Clock.count = 0; | ||
return Clock; | ||
}()); | ||
var DataNode = /** @class */ (function () { | ||
function DataNode(clock, value) { | ||
this.clock = clock; | ||
this.value = value; | ||
this.pending = NOTPENDING; | ||
this.log = null; | ||
function run(clock) { | ||
var running = RunningClock, count = 0; | ||
RunningClock = clock; | ||
clock.disposes.reset(); | ||
// for each batch ... | ||
while (clock.changes.count !== 0 || clock.subclocks.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) // don't tick on first run, or else we expire already scheduled updates | ||
clock.subtime++; | ||
clock.changes.run(applyDataChange); | ||
clock.subclocks.run(updateClock); | ||
clock.updates.run(updateNode); | ||
clock.disposes.run(dispose); | ||
// if there are still changes after excessive batches, assume runaway | ||
if (count++ > 1e5) { | ||
throw new Error("Runaway clock detected"); | ||
} | ||
} | ||
RunningClock = running; | ||
} | ||
return DataNode; | ||
}()); | ||
var ComputationNode = /** @class */ (function () { | ||
function ComputationNode(clock, fn, value) { | ||
this.clock = clock; | ||
this.fn = fn; | ||
this.value = value; | ||
this.state = CURRENT; | ||
this.source1 = null; | ||
this.source1slot = 0; | ||
this.sources = null; | ||
this.sourceslots = null; | ||
this.log = null; | ||
this.preclocks = null; | ||
this.owned = null; | ||
this.cleanups = null; | ||
this.age = this.clock.time(); | ||
function applyDataChange(data) { | ||
data.value = data.pending; | ||
data.pending = NOTPENDING; | ||
if (data.log) | ||
markComputationsStale(data.log); | ||
} | ||
return ComputationNode; | ||
}()); | ||
var Log = /** @class */ (function () { | ||
function Log() { | ||
this.node1 = null; | ||
this.node1slot = 0; | ||
this.nodes = null; | ||
this.nodeslots = null; | ||
} | ||
return Log; | ||
}()); | ||
var NodePreClockLog = /** @class */ (function () { | ||
function NodePreClockLog() { | ||
this.count = 0; | ||
this.clocks = []; // [clock], where clock.parent === node.clock | ||
this.ages = []; // clock.id -> node.age | ||
this.ucount = 0; // number of ancestor clocks with preclocks from this node | ||
this.uclocks = []; | ||
this.uclockids = []; | ||
} | ||
return NodePreClockLog; | ||
}()); | ||
var ClockPreClockLog = /** @class */ (function () { | ||
function ClockPreClockLog() { | ||
this.count = 0; | ||
this.clockcounts = []; // clock.id -> ref count | ||
this.clocks = []; // clock.id -> clock | ||
this.ids = []; // [clock.id] | ||
} | ||
return ClockPreClockLog; | ||
}()); | ||
var Queue = /** @class */ (function () { | ||
function Queue() { | ||
this.items = []; | ||
this.count = 0; | ||
} | ||
Queue.prototype.reset = function () { | ||
this.count = 0; | ||
}; | ||
Queue.prototype.add = function (item) { | ||
this.items[this.count++] = item; | ||
}; | ||
Queue.prototype.run = function (fn) { | ||
var items = this.items; | ||
for (var i = 0; i < this.count; i++) { | ||
fn(items[i]); | ||
items[i] = null; | ||
function markComputationsStale(log) { | ||
var node1 = log.node1, nodes = log.nodes; | ||
// mark all downstream nodes stale which haven't been already | ||
if (node1 !== null) | ||
markNodeStale(node1); | ||
if (nodes !== null) { | ||
for (var i = 0, len = nodes.length; i < len; i++) { | ||
markNodeStale(nodes[i]); | ||
} | ||
} | ||
this.count = 0; | ||
}; | ||
return Queue; | ||
}()); | ||
// Constants | ||
var NOTPENDING = {}; | ||
var CURRENT = 0; | ||
var STALE = 1; | ||
var RUNNING = 2; | ||
// "Globals" used to keep track of current system state | ||
var RootClock = new Clock(null); | ||
var RunningClock = null; | ||
var RunningNode = null; | ||
var Owner = null; | ||
var UNOWNED = new ComputationNode(RootClock, null, null); | ||
// Functions | ||
function logRead(from, to) { | ||
var fromslot, toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length; | ||
if (from.node1 === null) { | ||
from.node1 = to; | ||
from.node1slot = toslot; | ||
fromslot = -1; | ||
} | ||
else if (from.nodes === null) { | ||
from.nodes = [to]; | ||
from.nodeslots = [toslot]; | ||
fromslot = 0; | ||
} | ||
else { | ||
fromslot = from.nodes.length; | ||
from.nodes.push(to); | ||
from.nodeslots.push(toslot); | ||
} | ||
if (to.source1 === null) { | ||
to.source1 = from; | ||
to.source1slot = fromslot; | ||
} | ||
else if (to.sources === null) { | ||
to.sources = [from]; | ||
to.sourceslots = [fromslot]; | ||
} | ||
else { | ||
to.sources.push(from); | ||
to.sourceslots.push(fromslot); | ||
} | ||
} | ||
function logDataRead(data, to) { | ||
if (data.log === null) | ||
data.log = new Log(); | ||
logRead(data.log, to); | ||
} | ||
function logComputationRead(node, to) { | ||
if (node.log === null) | ||
node.log = new Log(); | ||
logRead(node.log, to); | ||
} | ||
function logNodePreClock(clock, to) { | ||
if (to.preclocks === null) | ||
to.preclocks = new NodePreClockLog(); | ||
else if (to.preclocks.ages[clock.id] === to.age) | ||
return; | ||
to.preclocks.ages[clock.id] = to.age; | ||
to.preclocks.clocks[to.preclocks.count++] = clock; | ||
} | ||
function logClockPreClock(sclock, rclock, rnode) { | ||
var clocklog = rclock.preclocks === null ? (rclock.preclocks = new ClockPreClockLog()) : rclock.preclocks, nodelog = rnode.preclocks === null ? (rnode.preclocks = new NodePreClockLog()) : rnode.preclocks; | ||
if (nodelog.ages[sclock.id] === rnode.age) | ||
return; | ||
nodelog.ages[sclock.id] = rnode.age; | ||
nodelog.uclocks[nodelog.ucount] = rclock; | ||
nodelog.uclockids[nodelog.ucount++] = sclock.id; | ||
var clockcount = clocklog.clockcounts[sclock.id]; | ||
if (clockcount === undefined) { | ||
clocklog.ids[clocklog.count++] = sclock.id; | ||
clocklog.clockcounts[sclock.id] = 1; | ||
clocklog.clocks[sclock.id] = sclock; | ||
} | ||
else if (clockcount === 0) { | ||
clocklog.clockcounts[sclock.id] = 1; | ||
clocklog.clocks[sclock.id] = sclock; | ||
} | ||
else { | ||
clocklog.clockcounts[sclock.id]++; | ||
} | ||
} | ||
function event() { | ||
// b/c we might be under a top level S.root(), have to preserve current root | ||
var owner = Owner; | ||
RootClock.subclocks.reset(); | ||
RootClock.updates.reset(); | ||
RootClock.subtime++; | ||
try { | ||
run(RootClock); | ||
} | ||
finally { | ||
RunningClock = RunningNode = null; | ||
Owner = owner; | ||
} | ||
} | ||
function toplevelComputation(node) { | ||
RunningClock = RootClock; | ||
RootClock.changes.reset(); | ||
RootClock.subclocks.reset(); | ||
RootClock.updates.reset(); | ||
try { | ||
node.value = node.fn(node.value); | ||
if (RootClock.changes.count > 0 || RootClock.subclocks.count > 0 || RootClock.updates.count > 0) { | ||
RootClock.subtime++; | ||
run(RootClock); | ||
function markNodeStale(node) { | ||
var time = node.clock.time(); | ||
if (node.age < time) { | ||
markClockStale(node.clock); | ||
node.age = time; | ||
node.state = STALE; | ||
node.clock.updates.add(node); | ||
if (node.owned !== null) | ||
markOwnedNodesForDisposal(node.owned); | ||
if (node.log !== null) | ||
markComputationsStale(node.log); | ||
} | ||
} | ||
finally { | ||
RunningClock = Owner = RunningNode = null; | ||
} | ||
} | ||
function run(clock) { | ||
var running = RunningClock, count = 0; | ||
RunningClock = clock; | ||
clock.disposes.reset(); | ||
// for each batch ... | ||
while (clock.changes.count !== 0 || clock.subclocks.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) { | ||
if (count > 0) | ||
clock.subtime++; | ||
clock.changes.run(applyDataChange); | ||
clock.subclocks.run(updateClock); | ||
clock.updates.run(updateNode); | ||
clock.disposes.run(dispose); | ||
// if there are still changes after excessive batches, assume runaway | ||
if (count++ > 1e5) { | ||
throw new Error("Runaway clock detected"); | ||
function markOwnedNodesForDisposal(owned) { | ||
for (var i = 0; i < owned.length; i++) { | ||
var child = owned[i]; | ||
child.age = child.clock.time(); | ||
child.state = CURRENT; | ||
if (child.owned !== null) | ||
markOwnedNodesForDisposal(child.owned); | ||
} | ||
} | ||
RunningClock = running; | ||
} | ||
function applyDataChange(data) { | ||
data.value = data.pending; | ||
data.pending = NOTPENDING; | ||
if (data.log) | ||
markComputationsStale(data.log); | ||
} | ||
function markComputationsStale(log) { | ||
var node1 = log.node1, nodes = log.nodes; | ||
// mark all downstream nodes stale which haven't been already | ||
if (node1 !== null) | ||
markNodeStale(node1); | ||
if (nodes !== null) { | ||
for (var i = 0, len = nodes.length; i < len; i++) { | ||
markNodeStale(nodes[i]); | ||
function markClockStale(clock) { | ||
var time = 0; | ||
if ((clock.parent !== null && clock.age < (time = clock.parent.time())) || clock.state === CURRENT) { | ||
if (clock.parent !== null) { | ||
clock.age = time; | ||
markClockStale(clock.parent); | ||
clock.parent.subclocks.add(clock); | ||
} | ||
clock.changes.reset(); | ||
clock.subclocks.reset(); | ||
clock.updates.reset(); | ||
clock.state = STALE; | ||
} | ||
} | ||
} | ||
function markNodeStale(node) { | ||
var time = node.clock.time(); | ||
if (node.age < time) { | ||
markClockStale(node.clock); | ||
node.age = time; | ||
node.state = STALE; | ||
node.clock.updates.add(node); | ||
if (node.owned !== null) | ||
markOwnedNodesForDisposal(node.owned); | ||
if (node.log !== null) | ||
markComputationsStale(node.log); | ||
} | ||
} | ||
function markOwnedNodesForDisposal(owned) { | ||
for (var i = 0; i < owned.length; i++) { | ||
var child = owned[i]; | ||
child.age = child.clock.time(); | ||
child.state = CURRENT; | ||
if (child.owned !== null) | ||
markOwnedNodesForDisposal(child.owned); | ||
} | ||
} | ||
function markClockStale(clock) { | ||
var time = 0; | ||
if ((clock.parent !== null && clock.age < (time = clock.parent.time())) || clock.state === CURRENT) { | ||
if (clock.parent !== null) { | ||
function updateClock(clock) { | ||
var time = clock.parent.time(); | ||
if (clock.age < time || clock.state === STALE) { | ||
if (clock.age < time) | ||
clock.state = CURRENT; | ||
if (clock.preclocks !== null) { | ||
for (var i = 0; i < clock.preclocks.ids.length; i++) { | ||
var preclock = clock.preclocks.clocks[clock.preclocks.ids[i]]; | ||
if (preclock) | ||
updateClock(preclock); | ||
} | ||
} | ||
clock.age = time; | ||
markClockStale(clock.parent); | ||
clock.parent.subclocks.add(clock); | ||
} | ||
clock.changes.reset(); | ||
clock.subclocks.reset(); | ||
clock.updates.reset(); | ||
clock.state = STALE; | ||
} | ||
} | ||
function updateClock(clock) { | ||
var time = clock.parent.time(); | ||
if (clock.age < time || clock.state === STALE) { | ||
if (clock.age < time) | ||
if (clock.state === RUNNING) { | ||
throw new Error("clock circular reference"); | ||
} | ||
else if (clock.state === STALE) { | ||
clock.state = RUNNING; | ||
run(clock); | ||
clock.state = CURRENT; | ||
if (clock.preclocks !== null) { | ||
for (var i = 0; i < clock.preclocks.ids.length; i++) { | ||
var preclock = clock.preclocks.clocks[clock.preclocks.ids[i]]; | ||
if (preclock) | ||
updateClock(preclock); | ||
} | ||
} | ||
clock.age = time; | ||
} | ||
if (clock.state === RUNNING) { | ||
throw new Error("clock circular reference"); | ||
} | ||
else if (clock.state === STALE) { | ||
clock.state = RUNNING; | ||
run(clock); | ||
clock.state = CURRENT; | ||
} | ||
} | ||
function updateNode(node) { | ||
if (node.state === STALE) { | ||
var owner = Owner, running = RunningNode, clock = RunningClock; | ||
Owner = RunningNode = node; | ||
RunningClock = node.clock; | ||
node.state = RUNNING; | ||
cleanup(node, false); | ||
node.value = node.fn(node.value); | ||
node.state = CURRENT; | ||
Owner = owner; | ||
RunningNode = running; | ||
RunningClock = clock; | ||
} | ||
} | ||
function cleanup(node, final) { | ||
var source1 = node.source1, sources = node.sources, sourceslots = node.sourceslots, cleanups = node.cleanups, owned = node.owned, preclocks = node.preclocks, i, len; | ||
if (cleanups !== null) { | ||
for (i = 0; i < cleanups.length; i++) { | ||
cleanups[i](final); | ||
function updateNode(node) { | ||
if (node.state === STALE) { | ||
var owner = Owner, running = RunningNode, clock = RunningClock; | ||
Owner = RunningNode = node; | ||
RunningClock = node.clock; | ||
node.state = RUNNING; | ||
cleanup(node, false); | ||
node.value = node.fn(node.value); | ||
node.state = CURRENT; | ||
Owner = owner; | ||
RunningNode = running; | ||
RunningClock = clock; | ||
} | ||
node.cleanups = null; | ||
} | ||
if (owned !== null) { | ||
for (i = 0; i < owned.length; i++) { | ||
dispose(owned[i]); | ||
function cleanup(node, final) { | ||
var source1 = node.source1, sources = node.sources, sourceslots = node.sourceslots, cleanups = node.cleanups, owned = node.owned, preclocks = node.preclocks, i, len; | ||
if (cleanups !== null) { | ||
for (i = 0; i < cleanups.length; i++) { | ||
cleanups[i](final); | ||
} | ||
node.cleanups = null; | ||
} | ||
node.owned = null; | ||
} | ||
if (source1 !== null) { | ||
cleanupSource(source1, node.source1slot); | ||
node.source1 = null; | ||
} | ||
if (sources !== null) { | ||
for (i = 0, len = sources.length; i < len; i++) { | ||
cleanupSource(sources.pop(), sourceslots.pop()); | ||
if (owned !== null) { | ||
for (i = 0; i < owned.length; i++) { | ||
dispose(owned[i]); | ||
} | ||
node.owned = null; | ||
} | ||
} | ||
if (preclocks !== null) { | ||
for (i = 0; i < preclocks.count; i++) { | ||
preclocks.clocks[i] = null; | ||
if (source1 !== null) { | ||
cleanupSource(source1, node.source1slot); | ||
node.source1 = null; | ||
} | ||
preclocks.count = 0; | ||
for (i = 0; i < preclocks.ucount; i++) { | ||
var upreclocks = preclocks.uclocks[i].preclocks, uclockid = preclocks.uclockids[i]; | ||
if (--upreclocks.clockcounts[uclockid] === 0) { | ||
upreclocks.clocks[uclockid] = null; | ||
if (sources !== null) { | ||
for (i = 0, len = sources.length; i < len; i++) { | ||
cleanupSource(sources.pop(), sourceslots.pop()); | ||
} | ||
} | ||
preclocks.ucount = 0; | ||
} | ||
} | ||
function cleanupSource(source, slot) { | ||
var nodes = source.nodes, nodeslots = source.nodeslots, last, lastslot; | ||
if (slot === -1) { | ||
source.node1 = null; | ||
} | ||
else { | ||
last = nodes.pop(); | ||
lastslot = nodeslots.pop(); | ||
if (slot !== nodes.length) { | ||
nodes[slot] = last; | ||
nodeslots[slot] = lastslot; | ||
if (lastslot === -1) { | ||
last.source1slot = slot; | ||
if (preclocks !== null) { | ||
for (i = 0; i < preclocks.count; i++) { | ||
preclocks.clocks[i] = null; | ||
} | ||
else { | ||
last.sourceslots[lastslot] = slot; | ||
preclocks.count = 0; | ||
for (i = 0; i < preclocks.ucount; i++) { | ||
var upreclocks = preclocks.uclocks[i].preclocks, uclockid = preclocks.uclockids[i]; | ||
if (--upreclocks.clockcounts[uclockid] === 0) { | ||
upreclocks.clocks[uclockid] = null; | ||
} | ||
} | ||
preclocks.ucount = 0; | ||
} | ||
} | ||
} | ||
function dispose(node) { | ||
node.clock = null; | ||
node.fn = null; | ||
node.log = null; | ||
node.preclocks = null; | ||
cleanup(node, true); | ||
} | ||
function cleanupSource(source, slot) { | ||
var nodes = source.nodes, nodeslots = source.nodeslots, last, lastslot; | ||
if (slot === -1) { | ||
source.node1 = null; | ||
} | ||
else { | ||
last = nodes.pop(); | ||
lastslot = nodeslots.pop(); | ||
if (slot !== nodes.length) { | ||
nodes[slot] = last; | ||
nodeslots[slot] = lastslot; | ||
if (lastslot === -1) { | ||
last.source1slot = slot; | ||
} | ||
else { | ||
last.sourceslots[lastslot] = slot; | ||
} | ||
} | ||
} | ||
} | ||
function dispose(node) { | ||
node.clock = null; | ||
node.fn = null; | ||
node.log = null; | ||
node.preclocks = null; | ||
cleanup(node, true); | ||
} | ||
return S; | ||
return S; | ||
}))); |
@@ -18,3 +18,4 @@ // Karma configuration | ||
files: [ | ||
'dist/withsubclocks.js', | ||
'dist/S.js', | ||
//'dist/withsubclocks.js', | ||
'spec/*.js', | ||
@@ -21,0 +22,0 @@ 'node_modules/benchmark/benchmark.js', |
{ | ||
"name": "s-js", | ||
"version": "0.4.8", | ||
"version": "0.4.9", | ||
"description": "S.js - simple, clean, fast reactive programming in Javascript", | ||
@@ -10,8 +10,8 @@ "main": "dist/S.js", | ||
"devDependencies": { | ||
"jasmine": "^2.8.0", | ||
"karma": "^1.7.0", | ||
"karma-chrome-launcher": "^2.1.1", | ||
"karma-jasmine": "^1.1.0", | ||
"rollup": "^0.51.5", | ||
"typescript": "^2.6.1" | ||
"jasmine": "^3.1.0", | ||
"karma": "^2.0.5", | ||
"karma-chrome-launcher": "^2.2.0", | ||
"karma-jasmine": "^1.1.2", | ||
"rollup": "^0.63.4", | ||
"typescript": "^2.9.2" | ||
}, | ||
@@ -18,0 +18,0 @@ "scripts": { |
176
README.md
# S.js | ||
S.js is a small library for performing **simple, clean, fast reactive programming** in Javascript. It aims for a simple mental model, a clean and expressive syntax, and fast execution. | ||
S.js is a small reactive programming library. It combines an automatic dependency graph with a synchronous execution engine. The goal is to make reactive programming simple, clean, and fast. | ||
In plain terms, S helps you **keep things up-to-date** in your program. S programs work like a spreadsheet: when data changes, S automatically updates downstream computations. | ||
An S app consists of *data signals* and *computations*: | ||
Here's a tiny example: | ||
```javascript | ||
const // | ||
a = S.data(1), // a() | 1 3 3 5 | ||
b = S.data(2), // b() | 2 2 4 6 | ||
c = S(() => a() + b()), // c() | 3 5 7 11 | ||
d = S(() => c() * a()); // t0 // d() | 3 15 21 55 | ||
a(3); // t1 // +------------------------> | ||
b(4); // t2 // t0 t1 t2 t3 | ||
S.freeze(() => { // | ||
a(5); // | ||
b(6); // | ||
}); // t3 // | ||
``` | ||
The timeline on the right shows how the values evolve at each instant. Initially (time t0), `c()` and `d()` are 3, but when `a()` changes to 3 (t1), they become 5 and 15. Ditto for t2 and t3. Every time `a()` or `b()` changes, S re-evaluates `c()` and `d()` to make sure they stay consistent. | ||
- *data signals* are created with `S.data(<value>)`. They are small containers for a piece of data that may change. | ||
To achieve this behavior, static data and computations must be converted to *signals*, which is a reactive term for a value that changes over time. S data signals are constructed by `S.data(<value>)` and computations by `S(() => <code>)`. Both return closures: call a signal to read its current value; pass a data signal a new value to change it. | ||
- *computations* are created with `S(() => <code>)`. They are kept up-to-date as data signals change. | ||
When an S computation runs, S records what signals it references, thereby creating a live dependency graph of running code. When data changes, S uses that graph to figure out what needs to be updated and in what order. | ||
Both kinds of signals are represented as small functions: call a signal to read its current value, pass a data signal a new value to update it. | ||
S has a small API. The example above shows `S.freeze()`, which aggregates multiple changes into a single step (t3). The full API is listed below. | ||
Beyond these two, S has a handful of utilities for controlling what counts as a change and how S responds. | ||
## S Features | ||
## Features | ||
S maintains a few useful behaviors while it runs. These features are designed to make it easier to reason about reactive programming: | ||
**Automatic Updates** - When data signal(s) change, S automatically re-runs any computations which read the old values. | ||
> **Automatic Dependencies** - No manual (un)subscription to change events. Dependencies in S are automatic and exact. | ||
> | ||
> **Guaranteed Currency** - No need to worry about how change propagates through the system. S insures that signals always return current and updated values. | ||
> | ||
> **Exact Updates** - No missed or redundant updates. Computations run exactly once per upstream change event. | ||
> | ||
> **A Unified Global Timeline** - No confusing nested or overlapping mutations from different sections of code. S apps advance through a series of discrete "instants" during which state is immutable. | ||
> | ||
> **Self-Extensible** - Computations can extend the system by creating new "child" computations. | ||
> | ||
> **Automatic Disposals** - No manual disposal of stale computations. "Child" computations are disposed automatically when their "parent" updates. | ||
**A Clear, Consistent Timeline** - S apps advance through a series of discrete "ticks." In each tick, all signals are guaranteed to return up-to-date values, and state is immutable until the tick completes. This greatly simplifies the often difficult task of reasoning about how change flows through a reactive app. | ||
For advanced cases, S provides capabilities for dealing with self-mutating code: | ||
**Batched Updates** - Multiple data signals can be changed in a single tick (aka "transactions"). | ||
> **Multi-Step Updates** - Computations can set data signals during their execution. These changes don't take effect until the current "instant" finishes, resulting in a multi-step update. | ||
> | ||
> **Partitionable Time** - Multi-step updates can run on a 'subclock,' meaning that surrounding code will only respond to final, at-rest values, not intermediate ones. | ||
**Automatic Disposals** - S computations can themselves create more computations, with the rule that "child" computations are disposed when their "parent" updates. This simple rule allows apps to be leak-free without the need for manual disposals. | ||
## An Example: TodoMVC in S (plus friends) | ||
What else, right? S is just a core library for dealing with change; it takes more to build an application. This example uses Surplus.js, aka "S plus" a few companion libraries. Most notably, it uses Surplus' JSX preprocessor for embedded DOM construction. | ||
```jsx | ||
const | ||
Todo = t => ({ // our Todo constructor | ||
title: S.data(t.title), // properties are data signals | ||
done: S.data(t.done) | ||
}), | ||
todos = SArray([]), // our array of todos | ||
newTitle = S.data(""), // title for new todos | ||
addTodo = () => { // push new title onto list | ||
todos.push(Todo({ title: newTitle(), done: false })); | ||
newTitle(""); // clear new title | ||
}, | ||
view = S.root(() => // declarative main view | ||
<div> | ||
<h2>Minimalist ToDos in Surplus</h2> | ||
<input type="text" fn={data(newTitle)}/> | ||
<a onClick={addTodo}> + </a> | ||
{todos.map(todo => // insert todo views | ||
<div> | ||
<input type="checkbox" fn={data(todo.done)}/> | ||
<input type="text" fn={data(todo.title)}/> | ||
<a onClick={() => todos.remove(todo)}>×</a> | ||
</div>)} | ||
</div>); | ||
## A Quick Example | ||
document.body.appendChild(view); // add view to document | ||
Start with the the world's smallest web app. It just sets the body of the page to the text "Hello, world!" | ||
```javascript | ||
let greeting = "Hello", | ||
name = "world"; | ||
document.body.textContent = `${greeting}, ${name}!`; | ||
``` | ||
Run on [CodePen](https://codepen.io/adamhaile/pen/ppvdGa?editors=0010). | ||
Some things to note: | ||
Now let's change the name. | ||
- There's no code to handle updating the application. Other than a liberal sprinkling of `()'s`, this could be static code. In the lingo, S enables declarative programming, where we focus on defining how things should be and S handles updating the app from one state to the next as our data changes. | ||
```javascript | ||
name = "reactivity"; | ||
``` | ||
- The Surplus library leverages S computations to construct the dynamic parts of the view (the '{ ... }' expressions). Whenever our data changes, S updates the affected parts of the DOM automatically. | ||
The page is now out of date, since it still has the old name, "Hello, world!" It didn't *react* to the data change. So let's fix that with S's wrappers. | ||
- S handles updates in as efficient a manner as possible: Surplus apps generally place at or near the top of the various web framework benchmarks (ToDoMVC, dbmonster, js-framework-benchmark, etc). | ||
```javascript | ||
let greeting = S.data("Hello"), | ||
name = S.data("world"); | ||
Reactive programs also have the benefit of an open structure that enables extensibility. For instance, we can add localStorage persistence with no changes to the code above and only a handful of new lines: | ||
S(() => document.body.textContent = `${greeting()}, ${name()}!`); | ||
``` | ||
The wrappers return small functions, called *signals*, which are containers for values that change over time. We read the current value of a signal by calling it, and if it's a data signal, we can set its next value by passing it in. | ||
```javascript | ||
if (localStorage.todos) // load stored todos on start | ||
todos(JSON.parse(localStorage.todos).map(Todo)); | ||
S(() => // store todos whenever they change | ||
localStorage.todos = JSON.stringify(todos().map(t => | ||
({ title: t.title(), done: t.done() }))); | ||
name("reactivity"); | ||
``` | ||
S knows that we read the old value of `name()` when we set the page text, so it re-runs that computation now that `name()` has changed. The page now reads "Hello, reactivity!" Yay! | ||
We've converted the plain code we started with into a small machine, able to detect and keep abreast of incoming changes. Our data signals define the kind of changes we might see, our computations how we respond to them. | ||
For longer examples see: | ||
- the [minimalist todos](https://github.com/adamhaile/surplus#example) application in the [Surplus](https://github.com/adamhaile/surplus) library | ||
- the [Surplus implementation of the "Realworld" demo](https://github.com/adamhaile/surplus-realworld) | ||
## API | ||
@@ -105,3 +70,3 @@ | ||
### `S.data(<value>)` | ||
Construct a data signal whose initial value is `<value>`. Read the current value of the data signal by calling it, set the next value by passing in a new one: | ||
A data signal is a small container for a single value. It's where information and change enter the system. Read the current value of a data signal by calling it, set the next value by passing in a new one: | ||
@@ -114,4 +79,6 @@ ```javascript | ||
Note that you are setting the **next** value: if you set a data signal in a context where time is frozen, like in an `S.freeze()` or a computation body, then your change will not take effect until time advances. This is because of S's unified global timeline of atomic instants: if your change took effect immediately, then there would be a before and after the change, breaking the instant in two: | ||
Data signals define the granularity of change in your application. Depending on your needs, you may choose to make them fine-grained – containing only an atomic value like a string, number, etc – or coarse – an entire large object in a single data signal. | ||
Note that when setting a data signal you are setting the **next** value: if you set a data signal in a context where time is frozen, like in an `S.freeze()` or a computation body, then your change will not take effect until time advances. This is because of S's unified global timeline of atomic instants: if your change took effect immediately, then there would be a before and after the change, breaking the instant in two: | ||
```javascript | ||
@@ -139,3 +106,3 @@ const name = S.data("sue"); | ||
Data signals created by `S.data()` *always* fire a change event when set, even if the new value is the same as the old: | ||
Data signals created by `S.data()` always fire a change event when set, even if the new value is the same as the old: | ||
@@ -154,3 +121,3 @@ ```javascript | ||
`S.value()` is identical to `S.data()` except that it *does not* fire a change event when set to the same value. It tells S "only the value of this data signal is important, not the set event." | ||
`S.value()` is identical to `S.data()` except that it does not fire a change event when set to the same value. It tells S "only the value of this data signal is important, not the set event." | ||
@@ -174,3 +141,3 @@ ```javascript | ||
### `S(() => <code>)` | ||
Construct a computation whose value is the result of the given `<code>`. | ||
A computation is a "live" piece of code which S will re-run as needed when data signals change. | ||
@@ -187,4 +154,6 @@ S runs the supplied function immediately, and as it runs, S automatically monitors any signals that it reads. To S, your function looks like: | ||
The referenced signals don't need to be in the lexical body of the function: they might be in a function called from your computation. All that matters is that evaluating the computation caused them to be read. Similarly, signals that are in the body of the function but aren't read due to conditional branches aren't recorded. This is true even if prior executions went down a different branch and did read them: only the last run matters, because only those signals were involved in creating the current value. | ||
The referenced signals don't need to be in the lexical body of the function: they might be in a function called from your computation. All that matters is that evaluating the computation caused them to be read. | ||
Similarly, signals that are in the body of the function but aren't read due to conditional branches aren't recorded. This is true even if prior executions went down a different branch and did read them: only the last run matters, because only those signals were involved in creating the current value. | ||
If some of those signals are computations, S guarantees that they will always return a current value. You'll never get a "stale" value, one that is affected by an upstream change but hasn't been updated yet. To your function, the world is always temporally consistent. | ||
@@ -210,3 +179,3 @@ | ||
Ask yourself: if a pure computation isn't called in your app, does it need to run? | ||
Ask yourself: if a pure computation isn't read in your app, does it need to run? | ||
@@ -254,8 +223,6 @@ The `S()` constructor is symmetric: it takes a paramless function that returns a value, and it returns a paramless function that returns the same value. The only difference is *when* that function runs. Without `S()`, it runs once per call. With `S()`, it runs once per change. | ||
### `S(val => <code>, <seed>)` | ||
Construct a reducing computation, whose new value is derived from the last one, staring with `<seed>`. | ||
Construct a reducing computation, whose new value is derived from the last one, staring with `<seed>`. For instance, this keeps a running sum of `foo()`: | ||
This alternate call signature for `S()` is useful when the next value of a computation depends on its previous one. | ||
```javascript | ||
const sumFoo = S(sum => sum + foo(), 0); // keeps a running sum of foo() | ||
const sumFoo = S(sum => sum + foo(), 0); | ||
``` | ||
@@ -280,3 +247,3 @@ | ||
Note that, unlike in some other libraries, `S.on()` does not change the parameters a function receives, only when it runs. Besides the first `<seed>` and last `<onchanges>` paramters, `S.on()` is identical to `S()`. | ||
Note that, unlike in some other libraries, `S.on()` does not change the parameters a function receives, only when it runs. Besides the first `<seed>` and last `<onchanges>` parameters, `S.on()` is identical to `S()`. | ||
@@ -296,3 +263,3 @@ ### Computation Roots | ||
As mentioned above, most computations in a fleshed-out S app are child computations, and their lifetimes are controlled by their parents. But there are two exceptions: | ||
As mentioned above, most computations in an S app are child computations, and their lifetimes are controlled by their parents. But there are two exceptions: | ||
@@ -306,30 +273,9 @@ 1. True top-level computations, like the `router()` mentioned above, are not under any parent. | ||
Be careful about orphaning computations created inside a computation: you're taking on the responsibility of knowing when they should be disposed, and if you get it wrong, you may have a "computation leak," where zombie computations accumulate and slowly bring your app down. | ||
A couple of cases where orphaning may be appropriate: | ||
An example of a case where orphaning may be appropriate is if you are creating objects inside a computation that outlive the value of that computation, and where those objects have computations to create some bit of behavior. | ||
1. The computation is tied to a particular object and only references data signals belonging to that object. In that case, orphaning it means it will last until the object is GC'd. | ||
```javascript | ||
// create a transfer whenever checking.balance() gets below 0 | ||
S(() => { | ||
if (checking.balance() < 0) { | ||
transfers.push(new Transfer(savings, checking, -checking.balance())); | ||
} | ||
}); | ||
2. The computation is tied to external, imperative events which cannot be easily converted into declarative state. So we create it inside an `S.root()` and call the supplied `dispose` function at the appropriate terminating event. | ||
let transferCounter = 0; | ||
class Transfer { | ||
constructor(from, to, amount) { | ||
this.id = transferCounter++; | ||
this.from = from; | ||
this.to = to; | ||
this.amount = amount; | ||
// transactions have a status ... | ||
this.status = S.value("pending"); | ||
// ... and we log whenever it changes | ||
S.on(this.status, () => log(`tx ${this.id} has status ${this.status()}`)); | ||
} | ||
} | ||
``` | ||
## Utilities | ||
@@ -414,3 +360,3 @@ | ||
Our side-effects are accumulating, when we want only the last one to be present. | ||
Our side-effects are accumulating, when we want only the last one to be present. Such a side-effect is called idempotent. | ||
@@ -453,3 +399,3 @@ `S.cleanup()` lets us fix that: | ||
1. Be careful not to create runaway mutation cycles, where you set a data signal you also read, thereby auto-invalidating your computation and causing another run, and another, and another, etc. This can be avoided by either using `S.on()` or `S.sample()` to suppress a dependency on the mutated signal, or by having a base condition that halts the mutation once the condition is met. Note that if you do have a runaway mutation, S will throw an exception after an excessively long number of follow-on updates (currently hardcoded to 100,000). | ||
1. Be careful not to create runaway mutation cycles, where your computation sets a data signal it also reads, thereby auto-invalidating itself and causing another run, and another, and another, etc. This can be avoided by either using `S.on()` or `S.sample()` to suppress a dependency on the mutated signal, or by having a base condition that halts the cycle once the condition is met. Note that if you do have a runaway mutation, S will throw an exception after an excessively long number of follow-on updates (currently hard-coded to 100,000). | ||
@@ -554,2 +500,2 @@ 2. Be aware that all the states of the mutated data signal are visible to other computations. So in the line above that sets `overdrawn()`, there will be a round of updates where `balance()` is less than 0 but `overdrawn()` has not yet been set to `true`, followed by a round in which it has. | ||
© 2018 Adam Haile, adam.haile@gmail.com. MIT License. | ||
© 2013-present Adam Haile, adam.haile@gmail.com. MIT License. |
@@ -16,2 +16,40 @@ describe("S.data", function () { | ||
}); | ||
it("does not throw if set to the same value twice in a freeze", function () { | ||
var d = S.data(1); | ||
S.freeze(() => { | ||
d(2); | ||
d(2); | ||
}); | ||
expect(d()).toBe(2); | ||
}); | ||
it("throws if set to two different values in a freeze", function () { | ||
var d = S.data(1); | ||
S.freeze(() => { | ||
d(2); | ||
expect(() => d(3)).toThrowError(/conflict/); | ||
}); | ||
}); | ||
it("does not throw if set to the same value twice in a computation", function () { | ||
S.root(() => { | ||
var d = S.data(1); | ||
S(() => { | ||
d(2); | ||
d(2); | ||
}); | ||
expect(d()).toBe(2); | ||
}); | ||
}); | ||
it("throws if set to two different values in a computation", function () { | ||
S.root(() => { | ||
var d = S.data(1); | ||
S(() => { | ||
d(2); | ||
expect(() => d(3)).toThrowError(/conflict/); | ||
}); | ||
}); | ||
}); | ||
}); |
// install S in global namespace | ||
(eval || null)("this").S = require('../../dist/withsubclocks'); | ||
//(eval || null)("this").S = require('../../dist/withsubclocks'); | ||
(eval || null)("this").S = require('../..'); |
195
src/S.ts
@@ -11,2 +11,4 @@ | ||
on<T>(ev : () => any, fn : (v : T) => T, seed : T, onchanges?: boolean) : () => T; | ||
effect<T>(fn : () => T) : void; | ||
effect<T>(fn : (v : T) => T, seed : T) : void; | ||
@@ -25,2 +27,9 @@ // Data signal constructors | ||
cleanup(fn : (final : boolean) => any) : void; | ||
// experimental - methods for creating new kinds of bindings | ||
isFrozen() : boolean; | ||
isListening() : boolean; | ||
makeDataNode<T>(value : T) : IDataNode<T>; | ||
makeComputationNode<T>(fn : () => T) : IComputationNode<T>; | ||
makeComputationNode<T>(fn : (val : T) => T, seed : T) : IComputationNode<T>; | ||
} | ||
@@ -34,36 +43,7 @@ | ||
// Public interface | ||
const S = <S>function S<T>(fn : (v : T) => T, value : T) : () => T { | ||
var owner = Owner, | ||
running = RunningNode; | ||
if (owner === null) console.warn("computations created without a root or parent will never be disposed"); | ||
var S = <S>function S<T>(fn : (v : T) => T, value : T) : () => T { | ||
var node = new ComputationNode(fn, value); | ||
Owner = RunningNode = node; | ||
if (RunningClock === null) { | ||
toplevelComputation(node); | ||
} else { | ||
node.value = node.fn!(node.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) owner.owned = [node]; | ||
else owner.owned.push(node); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
return function computation() { | ||
if (RunningNode !== null) { | ||
if (node.age === RootClock.time) { | ||
if (node.state === RUNNING) throw new Error("circular dependency"); | ||
else updateNode(node); // checks for state === STALE internally, so don't need to check here | ||
} | ||
logComputationRead(node, RunningNode); | ||
} | ||
return node.value; | ||
return node.current(); | ||
} | ||
@@ -79,3 +59,3 @@ }; | ||
var owner = Owner, | ||
root = fn.length === 0 ? UNOWNED : new ComputationNode(null, null), | ||
root = fn.length === 0 ? UNOWNED : new ComputationNode(null!, null), | ||
result : T = undefined!, | ||
@@ -135,2 +115,6 @@ disposer = fn.length === 0 ? null : function _dispose() { | ||
S.effect = function effect<T>(fn : (v : T) => T, value? : T) : void { | ||
new ComputationNode(fn, value); | ||
} | ||
S.data = function data<T>(value : T) : (value? : T) => T { | ||
@@ -140,27 +124,6 @@ var node = new DataNode(value); | ||
return function data(value? : T) : T { | ||
if (arguments.length > 0) { | ||
if (RunningClock !== null) { | ||
if (node.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== node.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + node.pending); | ||
} | ||
} else { // add to list of changes | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
} | ||
} else { // not batching, respond to change now | ||
if (node.log !== null) { | ||
node.pending = value; | ||
RootClock.changes.add(node); | ||
event(); | ||
} else { | ||
node.value = value; | ||
} | ||
} | ||
return value!; | ||
if (arguments.length === 0) { | ||
return node.current(); | ||
} else { | ||
if (RunningNode !== null) { | ||
logDataRead(node, RunningNode); | ||
} | ||
return node.value; | ||
return node.next(value); | ||
} | ||
@@ -235,2 +198,34 @@ } | ||
// experimental : exposing node constructors and some state | ||
S.makeDataNode = function makeDataNode(value) { | ||
return new DataNode(value); | ||
}; | ||
export interface IDataNode<T> { | ||
clock() : IClock; | ||
current() : T; | ||
next(value : T) : T; | ||
} | ||
export interface IComputationNode<T> { | ||
clock() : IClock; | ||
current() : T; | ||
} | ||
export interface IClock { | ||
time() : number; | ||
} | ||
S.makeComputationNode = function makeComputationNode(fn : any, seed? : any) { | ||
return new ComputationNode(fn, seed); | ||
}; | ||
S.isFrozen = function isFrozen() { | ||
return RunningClock !== null; | ||
}; | ||
S.isListening = function isListening() { | ||
return RunningNode !== null; | ||
}; | ||
// Internal implementation | ||
@@ -247,2 +242,6 @@ | ||
var RootClockProxy = { | ||
time: function () { return RootClock.time; } | ||
}; | ||
class DataNode { | ||
@@ -255,5 +254,40 @@ pending = NOTPENDING as any; | ||
) { } | ||
current() { | ||
if (RunningNode !== null) { | ||
logDataRead(this, RunningNode); | ||
} | ||
return this.value; | ||
} | ||
next(value : any) { | ||
if (RunningClock !== null) { | ||
if (this.pending !== NOTPENDING) { // value has already been set once, check for conflicts | ||
if (value !== this.pending) { | ||
throw new Error("conflicting changes: " + value + " !== " + this.pending); | ||
} | ||
} else { // add to list of changes | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
} | ||
} else { // not batching, respond to change now | ||
if (this.log !== null) { | ||
this.pending = value; | ||
RootClock.changes.add(this); | ||
event(); | ||
} else { | ||
this.value = value; | ||
} | ||
} | ||
return value!; | ||
} | ||
clock() { | ||
return RootClockProxy; | ||
} | ||
} | ||
class ComputationNode { | ||
fn : ((v : any) => any) | null; | ||
value : any; | ||
age : number; | ||
@@ -270,7 +304,48 @@ state = CURRENT; | ||
constructor( | ||
public fn : ((v : any) => any) | null, | ||
public value : any | ||
fn : (v : any) => any, | ||
value : any | ||
) { | ||
this.age = RootClock.time; | ||
this.fn = fn; | ||
this.value = value; | ||
this.age = RootClock.time; | ||
if (fn === null) return; | ||
var owner = Owner, | ||
running = RunningNode; | ||
if (owner === null) console.warn("computations created without a root or parent will never be disposed"); | ||
Owner = RunningNode = this; | ||
if (RunningClock === null) { | ||
toplevelComputation(this); | ||
} else { | ||
this.value = this.fn!(this.value); | ||
} | ||
if (owner && owner !== UNOWNED) { | ||
if (owner.owned === null) owner.owned = [this]; | ||
else owner.owned.push(this); | ||
} | ||
Owner = owner; | ||
RunningNode = running; | ||
} | ||
current() { | ||
if (RunningNode !== null) { | ||
if (this.age === RootClock.time) { | ||
if (this.state === RUNNING) throw new Error("circular dependency"); | ||
else updateNode(this); // checks for state === STALE internally, so don't need to check here | ||
} | ||
logComputationRead(this, RunningNode); | ||
} | ||
return this.value; | ||
} | ||
clock() { | ||
return RootClockProxy; | ||
} | ||
} | ||
@@ -318,3 +393,3 @@ | ||
Owner = null as ComputationNode | null, // owner for new computations | ||
UNOWNED = new ComputationNode(null, null); | ||
UNOWNED = new ComputationNode(null!, null); | ||
@@ -321,0 +396,0 @@ // Functions |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
225822
47
5160
488