S.js 16 KB

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