UNPKG

useless

Version:

Use Less. Do More. JavaScript on steroids.

525 lines (376 loc) 19.2 kB
"use strict"; const _ = require ('underscore') /* Generic functional primitives for dynamic code binding ======================================================================== */ _.tests.stream = { 'triggerOnce': function () { $assertEveryCalledOnce (function (mkay) { var t = _.triggerOnce () var f = function (_321) { $assert (_321 === 321); mkay () } t (f) t (f) t (321) t (123) }) }, 'observable': function () { /* Should accept value as constructor, it should be accessible by .value property */ var initedWithValue = _.observable (555) $assert (initedWithValue.value, 555) /* Should call with current value when upon binding */ $assertEveryCalledOnce (function (mkay) { var valueChanged = _.observable () valueChanged (999) valueChanged (function (_999) { $assert (_999, 999); mkay () }) }) /* Should call previously bound callback if changed */ $assertEveryCalled (function (mkay__3) { var valueChanged = _.observable () valueChanged (mkay__3) valueChanged (123) valueChanged (345) valueChanged (567) }) /* Should pass last distinct value as argument to callbacks, not calling if its not changed */ $assertEveryCalledOnce (function (mkay) { var valueChanged = _.observable () valueChanged (function (_111) { $assert (111, _111) mkay () }) valueChanged (111) valueChanged (111) }) /* Should pass previous value as second argument */ $assertEveryCalledOnce (function (mkay) { var valueChanged = _.observable (444) valueChanged (function (_666, _444) { if (_444) { $assert ([_666, _444], [666, 444]); mkay () } }) valueChanged (666) }) }, 'observable.when': function () { $assertEveryCalledOnce (function (mkay) { var value = _.observable (234) value.when ( 234, function () { mkay () }) }) // passing constant should work $assertEveryCalledOnce (function (mkay) { var value = _.observable () value.when (_.equals (432), function () { mkay () }) value (432) value (234) }) $assertNotCalled (function (mkay) { var value = _.observable () value.when (_.equals (432), function () { mkay () }) value (7) }) }, 'once': function () { $assertEveryCalledOnce (function (mkay) { var whenSomething = _.trigger () whenSomething.once (mkay) whenSomething.once (mkay) whenSomething () whenSomething () }) }, '_.gatherChanges': function () { var valueA = _.observable (), valueB = _.observable (), changes = [] _.gatherChanges (valueA, valueB, function (a, b) { changes.push ([a, b]) }) valueA (123) valueB (777) $assert (changes, [[123, undefined], [123, 777]]) }, 'context': function () { var trigger = _.extend (_.trigger (), { context: 42 }) trigger (function () { $assert (this, 42) }) trigger () }, '_.off (bound)': function () { var react = function () { $fail } var act = _.trigger (react) _.off (react) act () }, '_.off (stream)': function () { var fail = function () { $fail } var act = _.trigger (fail) _.off (act) act () }, '_.barrier (defaultListener)': function () { $assertEveryCalled (function (mkay) { _.barrier (function () { mkay () }) () }) }, /* Need to rewrite it for clariy */ 'all shit': function () { var obj = { somethingReady: _.barrier (), whenSomething: _.trigger () } /* Test conventional semantics (1:1 multicast) */ $assertEveryCalled (function (mkay1__2, mkay2__2) { obj.whenSomething (mkay1__2) // that's how you bind obj.whenSomething (mkay2__2) obj.whenSomething () // that's how you trigger it obj.whenSomething () }) /* Test unbinding */ $assertEveryCalledOnce (function (shouldCall) { var whenSomething = _.trigger () var shouldBeCalled = function () { shouldCall () }, shouldNotBeCalled = function () { $fail } whenSomething (shouldBeCalled) whenSomething (shouldNotBeCalled) whenSomething.off (shouldNotBeCalled) // that's how you unbind specific listeners whenSomething () }) /* Test 'barrier' semantics + test argument passing */ $assertEveryCalledOnce (function (mkay1, mkay2) { obj.somethingReady (function (x) { $assert (x === 'foo') // you may pass arguments to callbacks obj.somethingReady (x) // should not call anything mkay1 () }) obj.somethingReady (function (x) { $assert (x === 'foo') mkay2 () }) obj.somethingReady ('foo') }) // that's how you trigger it (may pass arguments) obj.somethingReady ('bar') // should not call anything }, 'call order consistency': function (done) { var abc = '' var put = function (x) { return _.barrier (function () { abc += x }) } var a = put ('a'), b = put ('b'), c = put ('c') var barr = _.barrier () barr (a) (function () { barr.postpones = true; barr (c); barr.postpones = false }) (b) // C is bound after B, so it should be executed after B barr (true) _.allTriggered ([a,b,c], function () { $assert (abc, 'abc'); done () }) }, '_.barrier reset': function () { var b = _.barrier () b ('not_42') b.reset () $assertEveryCalledOnce (function (mkay) { b (function (value) { mkay (); $assert (value, 42) }) b (42) }) }, '_.barrier (value)': function () { $assertEveryCalledOnce (function (mkay) { var willBe42 = _.barrier (42) $assert (willBe42.already) willBe42 (function (_42) { $assert (_42, 42); mkay () }) }) }, 'observable.item': function () { var items = _.observable ({ foo: 7, bar: 8 }) var foo = items.item ('foo') var bar = items.item ('bar') $assert (foo, items.item ('foo')) // should be same cached observable $assert (foo.value, 7) $assert (bar.value, 8) foo (77) $assert (foo.value, 77) $assert (items.value, { foo: 77, bar: 8 }) items ({ bar: 88 }) $assert (foo.value, undefined) $assert (bar.value, 88) $assert (items.value, { bar: 88 }) }, 'postpone works with _.trigger (regression)': function (done) { _.trigger (done).postpone () } } _.extend (_, { gatherChanges: function (...args) { var observables = _.isArray (args[0]) ? args[0] : _.initial (args) var accept = _.last (args) var gather = function (value) { accept.apply (this, _.pluck (observables, 'value')) } _.each (observables, function (read) { read (gather) }) }, allTriggered (triggers, then /* deprecated */) { return Promise.all (triggers.map (t => t.promise)).then (then) }, observableRef: function (...args) { return _.extend (_.observable.apply (this, args), { trackReference: true }) }, observable: function (...args) { const value = args[0] var stream = _.stream ({ isObservable: true, hasValue: args.length > 0, value: _.isFunction (value) ? undefined : value, read: function (schedule) { return function (returnResult) { if (stream.hasValue) { returnResult.call (this, stream.value) } schedule.call (this, returnResult) } }, write: function (returnResult) { return function (value) { if (stream.beforeWrite) { value = stream.beforeWrite (value) } if (!stream.hasValue || !(stream.trackReference ? (stream.value === value) : _.isEqual (stream.value, value))) { var prevValue = stream.value var hadValue = stream.hasValue stream.hasValue = true stream.value = value if (hadValue) { returnResult.call (this, false /* flush */, stream.value, prevValue) } else { returnResult.call (this, false /* flush */, stream.value) } } } } }) if (args.length) { stream.apply (this, args) } return _.extend (stream, { force: function (value) { stream.hasValue = false stream (arguments.length ? value : stream.value) }, then: function (fn) { var next = _.observable () next.beforeWrite = fn stream (function (x) { next.write (x) }) return next }, toggle: function () { return stream (!stream.value) }, tie: function (other) { stream (other) other (stream) return stream }, item: function (id) { var all = stream.itemObservables || (stream.itemObservables = {}) var item = all[id] if (!item) { item = all[id] = _.observable ((stream.value && stream.value)[id]) item (function (x) { var oldValue = (stream.value && stream.value[x]) if (oldValue !== x) { (stream.value || (stream.value = {}))[id] = x stream.force () } }) stream (function (items) { item.write (items[id]) }) } return item }, when: function (match, then) { var matchFn = _.isFunction (match) ? match : _.equals (match), alreadyCalled = false stream (function callee (...args) { if (matchFn (args[0])) { if (!alreadyCalled) { alreadyCalled = true stream.off (callee) then.apply (this, args) } else { /* log.w ('WTF') */ } } }) } }) }, barrier: function (defaultValue) { var defaultListener = undefined if (_.isFunction (defaultValue)) { defaultListener = defaultValue defaultValue = undefined } var barrier = _.stream ({ already: defaultValue !== undefined, value: defaultValue, reset: function () { barrier.already = false delete barrier.value }, write: function (returnResult) { return function (value) { if (!barrier.already) { barrier.already = true barrier.value = value } returnResult.call (this, true /* flush schedule */, barrier.value) } }, read: function (schedule) { return function (returnResult) { if (barrier.already) { ((barrier.postpones || barrier.commitingReads) ? // solves problem outlined in 'call order consistency' test returnResult.postponed : returnResult).call (this, barrier.value) } else { schedule.call (this, returnResult) } } } }) if (defaultListener) { barrier (defaultListener) } return barrier }, triggerOnce: $restArg (function (...args) { var stream = _.stream ({ read: function (schedule) { return function (listener) { if (stream.queue.indexOf (listener) < 0) { schedule.call (this, listener) } } }, write: function (writes) { return writes.partial (true) } }).apply (this, args); return stream }), trigger: $restArg (function (...args) { return _.stream ({ read: _.identity, write: function (writes) { return writes.partial (false) } }).apply (this, args) }), off: function (...args) { const fn = args[0], what = args[1] if (fn.queue) { if (args.length === 1) { fn.queue.off () } else { fn.queue.off (what) } } if (fn.queuedBy) { _.each (fn.queuedBy, function (queue) { queue.remove (fn) }) delete fn.queuedBy } }, stream: function (cfg_) { var cfg = cfg_ || {} var queue = _.extend ([], { off: function (...args) { const fn = args[0] if (this.length) { if (args.length === 0) { _.each (this, function (fn) { fn.queuedBy.remove (this) }, this) this.removeAll () } else { if (fn.queuedBy) { fn.queuedBy.remove (this) this.remove (fn) } } } } }) var self = undefined var scheduleRead = function (fn) { if (queue.indexOf (fn) < 0) { if (fn.queuedBy) { fn.queuedBy.push (queue) } else { fn.queuedBy = [queue] } queue.push (fn) } } var commitPendingReads = function (flush, ...args) { var context = self.context || this, schedule = queue.copy if (flush) { queue.off () } self.commitingReads = true for (var i = 0, n = schedule.length; i < n; i++) { (self.postpones ? schedule[i].postponed : schedule[i]).apply (context, args) } delete self.commitingReads } var write = cfg.write (commitPendingReads) var read = cfg.read (scheduleRead) /* I/O API (two-way) */ var frontEnd = function (...args) { const fn = args[0] if (_.isFunction (fn)) { read.call (this, fn) } else { write.apply (this, args) } return frontEnd } /* Once semantics */ var once = function (then) { if (!_.find (queue, function (f) { return f.onceWrapped_ === then })) { read (_.extend (function callee (v) { _.off (self, callee); then (v) }, { onceWrapped_: then })) } } /* Constructor */ self = _.extend ($restArg (frontEnd), cfg, { queue: queue, once: once, off: _.off.asMethod, read: read, write: write, postpone (...args) { self.postponed.apply (self.context, args) }, }) _.defineProperty (self, 'promise', () => new Promise (resolve => self (resolve))) return self } }) /* Observable.map (experimental) ======================================================================== */ _.deferTest (['stream', 'observable.map'], function () { /* General semantics */ var foo = _.observable ('foo'), bar = _.observable ('bar') var fooBar = _.observable.map ([foo, bar], _.appends ('42')) var results = [] fooBar (function (value) { results.push (value.copy) }) $assert (results, [['foo42', 'bar42']]) foo ('qux') bar ('zap') $assert (results, [['foo42', 'bar42'], ['qux42', 'bar42'], ['qux42', 'zap42']]) /* Works over objects */ _.observable.map ({ 'foo': _.observable ('bar') }) (function (obj) { $assert ({ 'foo': 'bar' }, obj) }) }, function () { _.observable.map = function (obj, fn) { fn = fn || _.identity var value = _.isArray (obj) ? new Array (obj.length) : {} var result = _.observable (value) _.each (obj, function (read, i) { read (function (x) { value[i] = fn (x, i); result.force (value) }) }) return result } _.observable.all = _.observable.map })