S.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // Public interface
  2. var S = function S(fn, value) {
  3. var node = new ComputationNode(fn, value);
  4. return function computation() {
  5. return node.current();
  6. };
  7. };
  8. // compatibility with commonjs systems that expect default export to be at require('s.js').default rather than just require('s-js')
  9. Object.defineProperty(S, 'default', { value: S });
  10. export default S;
  11. S.root = function root(fn) {
  12. var owner = Owner, root = fn.length === 0 ? UNOWNED : new ComputationNode(null, null), result = undefined, disposer = fn.length === 0 ? null : function _dispose() {
  13. if (RunningClock !== null) {
  14. RootClock.disposes.add(root);
  15. }
  16. else {
  17. dispose(root);
  18. }
  19. };
  20. Owner = root;
  21. if (RunningClock === null) {
  22. result = topLevelRoot(fn, disposer, owner);
  23. }
  24. else {
  25. result = disposer === null ? fn() : fn(disposer);
  26. Owner = owner;
  27. }
  28. return result;
  29. };
  30. function topLevelRoot(fn, disposer, owner) {
  31. try {
  32. return disposer === null ? fn() : fn(disposer);
  33. }
  34. finally {
  35. Owner = owner;
  36. }
  37. }
  38. S.on = function on(ev, fn, seed, onchanges) {
  39. if (Array.isArray(ev))
  40. ev = callAll(ev);
  41. onchanges = !!onchanges;
  42. return S(on, seed);
  43. function on(value) {
  44. var running = RunningNode;
  45. ev();
  46. if (onchanges)
  47. onchanges = false;
  48. else {
  49. RunningNode = null;
  50. value = fn(value);
  51. RunningNode = running;
  52. }
  53. return value;
  54. }
  55. };
  56. function callAll(ss) {
  57. return function all() {
  58. for (var i = 0; i < ss.length; i++)
  59. ss[i]();
  60. };
  61. }
  62. S.effect = function effect(fn, value) {
  63. new ComputationNode(fn, value);
  64. };
  65. S.data = function data(value) {
  66. var node = new DataNode(value);
  67. return function data(value) {
  68. if (arguments.length === 0) {
  69. return node.current();
  70. }
  71. else {
  72. return node.next(value);
  73. }
  74. };
  75. };
  76. S.value = function value(current, eq) {
  77. var data = S.data(current), age = -1;
  78. return function value(update) {
  79. if (arguments.length === 0) {
  80. return data();
  81. }
  82. else {
  83. var same = eq ? eq(current, update) : current === update;
  84. if (!same) {
  85. var time = RootClock.time;
  86. if (age === time)
  87. throw new Error("conflicting values: " + update + " is not the same as " + current);
  88. age = time;
  89. current = update;
  90. data(update);
  91. }
  92. return update;
  93. }
  94. };
  95. };
  96. S.freeze = function freeze(fn) {
  97. var result = undefined;
  98. if (RunningClock !== null) {
  99. result = fn();
  100. }
  101. else {
  102. RunningClock = RootClock;
  103. RunningClock.changes.reset();
  104. try {
  105. result = fn();
  106. event();
  107. }
  108. finally {
  109. RunningClock = null;
  110. }
  111. }
  112. return result;
  113. };
  114. S.sample = function sample(fn) {
  115. var result, running = RunningNode;
  116. if (running !== null) {
  117. RunningNode = null;
  118. result = fn();
  119. RunningNode = running;
  120. }
  121. else {
  122. result = fn();
  123. }
  124. return result;
  125. };
  126. S.cleanup = function cleanup(fn) {
  127. if (Owner !== null) {
  128. if (Owner.cleanups === null)
  129. Owner.cleanups = [fn];
  130. else
  131. Owner.cleanups.push(fn);
  132. }
  133. else {
  134. console.warn("cleanups created without a root or parent will never be run");
  135. }
  136. };
  137. // experimental : exposing node constructors and some state
  138. S.makeDataNode = function makeDataNode(value) {
  139. return new DataNode(value);
  140. };
  141. S.makeComputationNode = function makeComputationNode(fn, seed) {
  142. return new ComputationNode(fn, seed);
  143. };
  144. S.isFrozen = function isFrozen() {
  145. return RunningClock !== null;
  146. };
  147. S.isListening = function isListening() {
  148. return RunningNode !== null;
  149. };
  150. // Internal implementation
  151. /// Graph classes and operations
  152. var Clock = /** @class */ (function () {
  153. function Clock() {
  154. this.time = 0;
  155. this.changes = new Queue(); // batched changes to data nodes
  156. this.updates = new Queue(); // computations to update
  157. this.disposes = new Queue(); // disposals to run after current batch of updates finishes
  158. }
  159. return Clock;
  160. }());
  161. var RootClockProxy = {
  162. time: function () { return RootClock.time; }
  163. };
  164. var DataNode = /** @class */ (function () {
  165. function DataNode(value) {
  166. this.value = value;
  167. this.pending = NOTPENDING;
  168. this.log = null;
  169. }
  170. DataNode.prototype.current = function () {
  171. if (RunningNode !== null) {
  172. logDataRead(this, RunningNode);
  173. }
  174. return this.value;
  175. };
  176. DataNode.prototype.next = function (value) {
  177. if (RunningClock !== null) {
  178. if (this.pending !== NOTPENDING) { // value has already been set once, check for conflicts
  179. if (value !== this.pending) {
  180. throw new Error("conflicting changes: " + value + " !== " + this.pending);
  181. }
  182. }
  183. else { // add to list of changes
  184. this.pending = value;
  185. RootClock.changes.add(this);
  186. }
  187. }
  188. else { // not batching, respond to change now
  189. if (this.log !== null) {
  190. this.pending = value;
  191. RootClock.changes.add(this);
  192. event();
  193. }
  194. else {
  195. this.value = value;
  196. }
  197. }
  198. return value;
  199. };
  200. DataNode.prototype.clock = function () {
  201. return RootClockProxy;
  202. };
  203. return DataNode;
  204. }());
  205. var ComputationNode = /** @class */ (function () {
  206. function ComputationNode(fn, value) {
  207. this.state = CURRENT;
  208. this.source1 = null;
  209. this.source1slot = 0;
  210. this.sources = null;
  211. this.sourceslots = null;
  212. this.log = null;
  213. this.owned = null;
  214. this.cleanups = null;
  215. this.fn = fn;
  216. this.value = value;
  217. this.age = RootClock.time;
  218. if (fn === null)
  219. return;
  220. var owner = Owner, running = RunningNode;
  221. if (owner === null)
  222. console.warn("computations created without a root or parent will never be disposed");
  223. Owner = RunningNode = this;
  224. if (RunningClock === null) {
  225. toplevelComputation(this);
  226. }
  227. else {
  228. this.value = this.fn(this.value);
  229. }
  230. if (owner && owner !== UNOWNED) {
  231. if (owner.owned === null)
  232. owner.owned = [this];
  233. else
  234. owner.owned.push(this);
  235. }
  236. Owner = owner;
  237. RunningNode = running;
  238. }
  239. ComputationNode.prototype.current = function () {
  240. if (RunningNode !== null) {
  241. if (this.age === RootClock.time) {
  242. if (this.state === RUNNING)
  243. throw new Error("circular dependency");
  244. else
  245. updateNode(this); // checks for state === STALE internally, so don't need to check here
  246. }
  247. logComputationRead(this, RunningNode);
  248. }
  249. return this.value;
  250. };
  251. ComputationNode.prototype.clock = function () {
  252. return RootClockProxy;
  253. };
  254. return ComputationNode;
  255. }());
  256. var Log = /** @class */ (function () {
  257. function Log() {
  258. this.node1 = null;
  259. this.node1slot = 0;
  260. this.nodes = null;
  261. this.nodeslots = null;
  262. }
  263. return Log;
  264. }());
  265. var Queue = /** @class */ (function () {
  266. function Queue() {
  267. this.items = [];
  268. this.count = 0;
  269. }
  270. Queue.prototype.reset = function () {
  271. this.count = 0;
  272. };
  273. Queue.prototype.add = function (item) {
  274. this.items[this.count++] = item;
  275. };
  276. Queue.prototype.run = function (fn) {
  277. var items = this.items;
  278. for (var i = 0; i < this.count; i++) {
  279. fn(items[i]);
  280. items[i] = null;
  281. }
  282. this.count = 0;
  283. };
  284. return Queue;
  285. }());
  286. // Constants
  287. var NOTPENDING = {}, CURRENT = 0, STALE = 1, RUNNING = 2;
  288. // "Globals" used to keep track of current system state
  289. var RootClock = new Clock(), RunningClock = null, // currently running clock
  290. RunningNode = null, // currently running computation
  291. Owner = null, // owner for new computations
  292. UNOWNED = new ComputationNode(null, null);
  293. // Functions
  294. function logRead(from, to) {
  295. var fromslot, toslot = to.source1 === null ? -1 : to.sources === null ? 0 : to.sources.length;
  296. if (from.node1 === null) {
  297. from.node1 = to;
  298. from.node1slot = toslot;
  299. fromslot = -1;
  300. }
  301. else if (from.nodes === null) {
  302. from.nodes = [to];
  303. from.nodeslots = [toslot];
  304. fromslot = 0;
  305. }
  306. else {
  307. fromslot = from.nodes.length;
  308. from.nodes.push(to);
  309. from.nodeslots.push(toslot);
  310. }
  311. if (to.source1 === null) {
  312. to.source1 = from;
  313. to.source1slot = fromslot;
  314. }
  315. else if (to.sources === null) {
  316. to.sources = [from];
  317. to.sourceslots = [fromslot];
  318. }
  319. else {
  320. to.sources.push(from);
  321. to.sourceslots.push(fromslot);
  322. }
  323. }
  324. function logDataRead(data, to) {
  325. if (data.log === null)
  326. data.log = new Log();
  327. logRead(data.log, to);
  328. }
  329. function logComputationRead(node, to) {
  330. if (node.log === null)
  331. node.log = new Log();
  332. logRead(node.log, to);
  333. }
  334. function event() {
  335. // b/c we might be under a top level S.root(), have to preserve current root
  336. var owner = Owner;
  337. RootClock.updates.reset();
  338. RootClock.time++;
  339. try {
  340. run(RootClock);
  341. }
  342. finally {
  343. RunningClock = RunningNode = null;
  344. Owner = owner;
  345. }
  346. }
  347. function toplevelComputation(node) {
  348. RunningClock = RootClock;
  349. RootClock.changes.reset();
  350. RootClock.updates.reset();
  351. try {
  352. node.value = node.fn(node.value);
  353. if (RootClock.changes.count > 0 || RootClock.updates.count > 0) {
  354. RootClock.time++;
  355. run(RootClock);
  356. }
  357. }
  358. finally {
  359. RunningClock = Owner = RunningNode = null;
  360. }
  361. }
  362. function run(clock) {
  363. var running = RunningClock, count = 0;
  364. RunningClock = clock;
  365. clock.disposes.reset();
  366. // for each batch ...
  367. while (clock.changes.count !== 0 || clock.updates.count !== 0 || clock.disposes.count !== 0) {
  368. if (count > 0) // don't tick on first run, or else we expire already scheduled updates
  369. clock.time++;
  370. clock.changes.run(applyDataChange);
  371. clock.updates.run(updateNode);
  372. clock.disposes.run(dispose);
  373. // if there are still changes after excessive batches, assume runaway
  374. if (count++ > 1e5) {
  375. throw new Error("Runaway clock detected");
  376. }
  377. }
  378. RunningClock = running;
  379. }
  380. function applyDataChange(data) {
  381. data.value = data.pending;
  382. data.pending = NOTPENDING;
  383. if (data.log)
  384. markComputationsStale(data.log);
  385. }
  386. function markComputationsStale(log) {
  387. var node1 = log.node1, nodes = log.nodes;
  388. // mark all downstream nodes stale which haven't been already
  389. if (node1 !== null)
  390. markNodeStale(node1);
  391. if (nodes !== null) {
  392. for (var i = 0, len = nodes.length; i < len; i++) {
  393. markNodeStale(nodes[i]);
  394. }
  395. }
  396. }
  397. function markNodeStale(node) {
  398. var time = RootClock.time;
  399. if (node.age < time) {
  400. node.age = time;
  401. node.state = STALE;
  402. RootClock.updates.add(node);
  403. if (node.owned !== null)
  404. markOwnedNodesForDisposal(node.owned);
  405. if (node.log !== null)
  406. markComputationsStale(node.log);
  407. }
  408. }
  409. function markOwnedNodesForDisposal(owned) {
  410. for (var i = 0; i < owned.length; i++) {
  411. var child = owned[i];
  412. child.age = RootClock.time;
  413. child.state = CURRENT;
  414. if (child.owned !== null)
  415. markOwnedNodesForDisposal(child.owned);
  416. }
  417. }
  418. function updateNode(node) {
  419. if (node.state === STALE) {
  420. var owner = Owner, running = RunningNode;
  421. Owner = RunningNode = node;
  422. node.state = RUNNING;
  423. cleanup(node, false);
  424. node.value = node.fn(node.value);
  425. node.state = CURRENT;
  426. Owner = owner;
  427. RunningNode = running;
  428. }
  429. }
  430. function cleanup(node, final) {
  431. var source1 = node.source1, sources = node.sources, sourceslots = node.sourceslots, cleanups = node.cleanups, owned = node.owned, i, len;
  432. if (cleanups !== null) {
  433. for (i = 0; i < cleanups.length; i++) {
  434. cleanups[i](final);
  435. }
  436. node.cleanups = null;
  437. }
  438. if (owned !== null) {
  439. for (i = 0; i < owned.length; i++) {
  440. dispose(owned[i]);
  441. }
  442. node.owned = null;
  443. }
  444. if (source1 !== null) {
  445. cleanupSource(source1, node.source1slot);
  446. node.source1 = null;
  447. }
  448. if (sources !== null) {
  449. for (i = 0, len = sources.length; i < len; i++) {
  450. cleanupSource(sources.pop(), sourceslots.pop());
  451. }
  452. }
  453. }
  454. function cleanupSource(source, slot) {
  455. var nodes = source.nodes, nodeslots = source.nodeslots, last, lastslot;
  456. if (slot === -1) {
  457. source.node1 = null;
  458. }
  459. else {
  460. last = nodes.pop();
  461. lastslot = nodeslots.pop();
  462. if (slot !== nodes.length) {
  463. nodes[slot] = last;
  464. nodeslots[slot] = lastslot;
  465. if (lastslot === -1) {
  466. last.source1slot = slot;
  467. }
  468. else {
  469. last.sourceslots[lastslot] = slot;
  470. }
  471. }
  472. }
  473. }
  474. function dispose(node) {
  475. node.fn = null;
  476. node.log = null;
  477. cleanup(node, true);
  478. }