useless
Version:
Use Less. Do More. JavaScript on steroids.
1,229 lines (894 loc) • 50.5 kB
JavaScript
"use strict";
const _ = require ('underscore')
/* What for:
- Hierarchy management (parent-child relationship)
- Destructors ('destroy' method), propagating through hierarchy
- bindable on $prototypes, auto-disconnecting if involved component gets destroyed
- trigger/barrier on $prototypes, auto-disconnecting if involved component gets destroyed
Component facility provides unified mechanics for deinitialization, thus allowing
to freely combine distinct components into more complex structure with no need to
know how to specifically deinitialize each of them.
Use to define highly configurable/reusable objects having limited lifetime, holding
system resources and organizing into hierarchies, e.g. UI components, like dialogs,
menus, embeddable data views. They hold DOM references and bound events, so one needs
to properly free those resources during deinitialization. Case studies:
- For example, a pop-up menu could render itself into top-level 'document' element,
so just by destroying its parent component's DOM, things created by this pop-up
wont be destroyed, and that's why explicit 'destroy' method is needed. With
Component, you call 'destroy' on parent component, and it propagates to child
components automatically, triggering their 'destroy' methods.
- A component could dynamically bind to other components with help of $bindable and
$trigger facilities. If such component gets destroyed, those links became invalid
and should be removed, otherwise it's considered as 'memory leak'. Component handles
such situation, removing those links if any involved component gets destroyed.
Component could be considered as basic tool for dynamic code binding at macro level,
promoting functional code binding tools (defined in dynamic/stream.js) to $prototypes.
*/
_.tests.component = {
/* - Passing config to constructor will extend constructed instance with that object
- Component constructors exhibit CPS interface (last function argument interprets as continuation)
*/
'constructor([cfg, ][then])': function () { $assertNotCalled (function (mkay) {
var Compo = $component ({})
/* 1. constructor (cfg)
*/
$assertMatches (new Compo ({ foo: 42 }), { foo: 42 })
/* 2. constructor (then)
*/
//new Compo (mkay)
/* 3. constructor (cfg, then)
*/
/*$assertMatches (new Compo ({ foo: 42 }, mkay), { foo: 42 })*/ }) },
/* init() should be entry point to a component, calling at constructor by default
*/
'init': function () { $assertEveryCalledOnce (function (mkay) {
$singleton (Component, {
init: function () {
mkay () } }) }) },
/* init(then) means your initialization is defined in CPS style
*/
/*'CPS init': function () { $assertEveryCalled (function (compo1, compo2) {
var Compo = $prototype ({
init: function (then) { // you're required to call then, to complete init
then () } })
var compo = new Compo (function () {
compo1 () })
var compo2 = new Compo ({ _42: 42 }, function () {
$assert (this._42, 42)
compo2 () }) }) },*/
/* constructor overriding is prohibited (by $final), use init() API for configuration means
*/
'no constructor overriding': function () { $assertThrows (function () {
$singleton (Component, {
constructor: function () {} }) }) },
/* If you don't want init() to be called at constructor (to call it manually later),
pass init:false to constructor's config
*/
'manual init()': function () { $assertNotCalled (function (fail) {
var Compo = $component ({ init: function () { fail () } })
var compo = new Compo ({ init: false })
$assert (typeof compo.init, 'function') }) }, // shouldn't be replaced by false
/* initialized is a _.barrier that opens after initialization
*/
'initialized (barrier)': function () {
var Compo = $component ({ init: function () {} })
var compo = new Compo ({ init: false })
$assert (!compo.initialized.already)
$assertEveryCalledOnce (function (mkay) {
compo.initialized (function () { mkay () })
compo.init () }) },
/* 'thiscall' semantics for methods (which can be defined by a variety of ways)
*/
'thiscall for methods': function () {
$assertEveryCalledOnce (function (prototypeMethod, instanceMethod) {
var instance = null
var Compo = new $component ({
prototypeMethod: function () { $assert (this === instance); prototypeMethod () } })
instance = new Compo ({
instanceMethod: function () { $assert (this === instance); instanceMethod () } })
instance.prototypeMethod.call (null)
instance.instanceMethod.call (null) }) },
/* Pluggable init/destroy with $traits (tests all combinations of CPS / sequential style method calling)
*/
'pluggable init with $traits': function () { var A, B, C, D
var A = $trait ({
beforeInit: function () { A = true; return Promise.resolve () },
afterInit: function () { B = true; return Promise.resolve () } })
var B = $trait ({
beforeInit: function () { C = true; },
afterInit: function () { D = true; }
})
var C = $component ({
$traits: [B, A] })
return (new C ()).initialized.promise.then (function () {
$assert (A,B,C,D,true) }) },
/* $defaults is convenient macro to extract _.defaults thing from init() to definition level
*/
'$defaults basic': function () {
var Compo = $component ({ $defaults: { foo: 42 }})
$assert ($untag (Compo.$definition.$defaults), { foo: 42 })
$assert ( Compo. $defaults, { foo: 42 })
var Compo2 = $component ({ $traits: [$trait ({ $defaults: { foo: 11 } })], $defaults: { } })
$assert ($untag (Compo2.$definition.$defaults), { foo: 11 })
$assert ( Compo2. $defaults, { foo: 11 })
},
'$defaults': function () {
var Trait = $trait ({ $defaults: { pff: 'pff', inner: { fromTrait: 1 } }})
var Base = $component ({ $defaults: { foo: 12, qux: 'override me', inner: { fromBase: 1 } } })
var Derived = $extends (Base, {
$traits: [Trait],
$defaults: { bar: 34, qux: 'overriden', inner: { fromDerived: 1 } } })
//$assert (Derived.$ownDefaults,
// { bar: 34, qux: 'overriden', inner: { fromDerived: 1 } } )
/* TODO: fix bug not allowing derived to not have $defaults
var Derived2 = $extends (Derived, {}) */
$assert (new Derived ().inner !== new Derived ().inner) // should clone $defaults at instance construction
$assertMatches (new Derived ({ pff: 'overriden from cfg' }), { pff: 'overriden from cfg', foo: 12, bar: 34, qux: 'overriden', inner: { fromTrait: 1, fromBase: 1, fromDerived: 1 } }) },
'$defaults cloning semantics': function () { var set = new Set ([1,2,3])
var S = $component ({ $defaults: { foo: set,
bar: new Set () } })
var s = new S ()
$assert (s.foo instanceof Set,
s.bar instanceof Set,
true)
$assert (s.foo !== set)
},
/* Use $requires to specify required config params along with their type signatures
*/
'$requires': function () {
var SomeType = $prototype ()
var CompoThatRequires = $component ({
$requires: {
foo: SomeType, // requires foo to be instance of SomeType
ffu: { a: 'number', b: 'string' }, // breakdown test
bar: 'number',
qux: ['number'],
baz: _.not (_.isEmpty) } }) // custom requirement predicate
var DerivedCompoThatRequiresMore =
$extends (CompoThatRequires, {
$requires: { more: 'string' } })
$assertFails (function () {
new CompoThatRequires ({ baz: {} }) }) // $requires behaves like assertion in case of failure
$assertFails (function () {
new DerivedCompoThatRequiresMore ({ more: 'hey how about other requirements' }) })
new DerivedCompoThatRequiresMore ({
foo: new SomeType (),
bar: 42,
qux: [1,2,3],
more: 'blah blah',
ffu: { a: 1, b: '2' },
baz: 'blahblah' }) },
/* $overrideThis is a macro that requires a method to be overriden
*/
/*'overrideThis': function () {
$assertThrows (function () { $singleton (Component, { foo: $overrideThis (function () {}) }) },
_.matches ({ message: 'foo should be overriden' })) },*/
/* $bindable lifts _.bindable to Component level, opening new venues to hooking onto existing impl,
in ad-hoc way, with no need to specify hard-coded callback structure beforehand.
Use to implement common beforeXXX and afterXXX semantics.
*/
'$bindable': function () { $assertEveryCalledOnce (function (method, before, after) {
var compo = $singleton (Component, {
method: $bindable (function (x) { method ()
return 42 }) })
compo.method.onBefore (function (_5) { before ()
$assert (this === compo)
$assert (_5, 5) })
compo.method.onAfter (function (_5, _result) { after ()
$assert (this === compo)
$assert (_5, 5)
$assert (_result, 42) })
$assert (compo.method (5), 42) }) },
/* Trigger has many names in outer world, like Event, Signal (and legion of
many other misleading buzzwords).
In our implementation, Trigger is a partial case of 'stream' concept, which
is a highly abstract functional I/O primitive for multicasting of data/events).
See dynamic/stream.js for its amazingly simple implementation.
1. If called with some value arguments (or no argument), it performs
multicast of these arguments to all bound listeners (readers).
In terms of 'streams' that operation is called 'write'.
2. If called with function argument, it adds that function to the wait
queue mentioned before. In terms of 'streams' it is called 'read'.
Component manages those streams (defined by $-syntax at its prototype definition),
auto-disconnecting bound methods, so that no method of Component bound
to such streams will ever be called after destroy().
*/
'$trigger': function () { $assertEveryCalled (function (mkay__2) {
var compo = $singleton (Component, {
mouseMoved: $trigger () })
compo.mouseMoved (function (x, y) { $assert ([x, y], [7, 12]); mkay__2 () })
compo.mouseMoved (7, 12)
compo.mouseMoved (7, 12) }) },
'init streams from config': function () { $assertEveryCalled (function (atDefinition, atInit) {
var Compo = $component ({
mouseMoved: $trigger (atDefinition),
init: function () {
this.mouseMoved () } })
new Compo ({ mouseMoved: atInit }) }) },
/* A variation of trigger. On 'write' operation, it flushes wait queue, so
no callback bound previously gets called in future (until explicitly
queued again by 'read' operation).
*/
'$triggerOnce': function () {
var compo = $singleton (Component, {
somthingHappened: $triggerOnce () })
$assertEveryCalled (function (first, second) {
compo.somthingHappened (function (what) { $assert (what, 'somthin'); first () })
compo.somthingHappened (function (what) { $assert (what, 'somthin'); second () })
compo.somthingHappened ('somthin') }) },
/* Another variation of stream, having 'memory fence / memory barrier' semantics,
widely known as synchronization primitive in concurrent programming.
1. At first, barrier is in closed state, putting any callback passed to it
to a queue.
2. When barrier is called with value argument, it state changes to 'opened',
triggering all queued callbacks with that value argument passed in.
3. After barrier had opened, any futher callback gets called immediately
with that value argument passed before, i.e. short-circuits.
*/
'$barrier': function () { $assertEveryCalled (function (early, lately) {
var compo = $singleton (Component, {
hasMessage: $barrier () })
compo.hasMessage (function (_msg) { $assert (_msg, 'mkay'); early () })
compo.hasMessage ('mkay')
compo.hasMessage (function (_msg) { $assert (_msg, 'mkay'); lately () }) }) },
/* $observableProperty is a powerful compound mechanism for data-driven dynamic
code binding, built around streams described previously.
*/
'$observableProperty': function () { $assertEveryCalled (function (
fromConstructor,
fromConfig,
fromLateBoundListener,
fromDefinition,
fromListenerOnlyVariant) {
var Compo = $component ({
color: $observableProperty (),
smell: $observableProperty (),
shape: $observableProperty ('round', function (now) { $assert (now, 'round'); fromDefinition () }),
size: $observableProperty (function (x) { $assert (x, 42); fromListenerOnlyVariant () }),
init: function () {
this.colorChange (function (now, was) { if (was) { fromConstructor ()
$assert ([now, was], ['green', 'blue']) } }) } })
var compo = new Compo ({
color: 'blue',
size: 42,
colorChange: function (now, was) { if (was) { fromConfig ()
$assert ([now, was], ['green', 'blue']) } } })
//console.log (compo.constructor.$definition)
compo.smellChange (function (now, was) { fromLateBoundListener ()
$assert (compo.smell, now, 'bad')
$assert (undefined, was) })
compo.color = 'green'
compo.smell = 'bad' }) },
/* $observableProperty automatically calls prototype constructor if supplied with non-prototype instance data
*/
'$observableProperty (Prototype)': function () {
var Compo = $component ({
position: $observableProperty (Vec2.zero),
init: function () {
this.positionChange (function (v) {
$assertTypeMatches (v, Vec2)
$assert (v.y, 42) }) } })
var compo = new Compo ({ position: { x: 10, y: 42 }}) // supply POD value from constructor
compo.position = { x: 20, y: 42 } }, // supply POD value from property accessor
'binding to streams with traits': function () {
Meta.globalTag ('dummy')
$assertEveryCalled (function (mkay1, mkay2) { var this_ = undefined
var Trait = $trait ({
somethingHappened: $trigger () })
var Other = $trait ({
somethingHappened: $dummy (function (_42) { $assert (this, this_); $assert (_42, 42); mkay1 () }) })
var Compo = $component ({
$traits: [Trait, Other],
somethingHappened: function (_42) { $assert (this, this_); $assert (_42, 42); mkay2 () } })
this_ = new Compo ()
this_.somethingHappened (42) }) },
'binding to bindables with traits': function () {
$assertCallOrder (function (beforeCalled, interceptCalled, bindableCalled, afterCalled) { var this_ = undefined
var Trait = $trait ({
doSomething: $bindable (function (x) { $assert (this, this_); bindableCalled () }) })
var Other = $trait ({
beforeDoSomething: function (_42) { $assert (this, this_); $assert (_42, 42); beforeCalled () },
interceptDoSomething: function (_42, impl) { interceptCalled (); $assert (this, this_); return impl (_42) } })
var Compo = $component ({
$traits: [Trait, Other],
afterDoSomething: function (_42) { $assert (this, this_); $assert (_42, 42); afterCalled () } })
this_ = new Compo ()
this_.doSomething (42) }) },
'binding to observable properties with traits': function () {
$assertEveryCalled (function (one, two) { var this_ = undefined
var Trait = $trait ({
someValue: $observableProperty (42) })
var Other = $trait ({
someValue: function (_42) { one () } })
var Compo = $component ({
$traits: [Trait, Other],
someValue: function (_42) { two () } })
this_ = new Compo ()
$assert (_.isFunction (this_.someValueChange))
this_.someValue = 33 }) },
'hierarchy management': function () { $assertEveryCalled (function (mkay__9) {
var Compo = $extends (Component, {
init: function () { mkay__9 () },
destroy: function () { mkay__9 () } })
var parent = new Compo ().attach (
new Compo ().attach (
new Compo ()))
var parrot = new Compo ()
.attachTo (parent)
.attachTo (parent)
$assert (parrot.attachedTo === parent)
$assert (parrot.detach ().attachedTo === undefined)
var carrot = new Compo ()
parent.attach (carrot)
parent.attach (carrot)
parent.destroy () })},
'thiscall for streams': function () {
var compo = $singleton (Component, {
trig: $trigger () })
compo.trig (function () {
$assert (this === compo) })
compo.trig.call ({}) },
'$defaults can set $observableProperty': function () {
var compo = $singleton (Component, {
twentyFour: $observableProperty (42),
$defaults: { twentyFour: 24 } })
$assertEveryCalledOnce (function (mkay) {
compo.twentyFourChange (function (val) { $assert (val, 24); mkay (); }) }) },
'defer init with $defaults': function () {
var compo = $singleton (Component, {
$defaults: { init: false },
init: function () { } })
compo.init () },
'stream members should be available at property setters when inited from config': function () {
var compo = new ($component ({
ready: $barrier (),
value: $property ({
set: function (_42) { $assertTypeMatches (this.ready, 'function') } }) })) ({ value: 42 }) },
'observableProperty.force (regression)': function () { $assertEveryCalled (function (mkay__2) {
var compo = $singleton (Component, {
prop: $observableProperty () })
compo.prop = 42
compo.propChange (function (value) {
$assert (value, 42)
$assert (this === compo)
mkay__2 () })
compo.propChange.force () }) },
'two-argument $observableProperty syntax': function () {
$assertEveryCalled (function (mkay) {
var compo = $singleton (Component, {
prop: $observableProperty (42, function (value) { mkay ()
if (compo) {
$assert (this === compo)
$assert (value === compo.prop) } }) })
compo.prop = 43 }) },
'two-argument $observable': function () {
$assertEveryCalled (function (mkay) {
$assert ('foo', $singleton (Component, {
foo: $observable ('foo', function (x) { $assert (x, 'foo'); mkay () }) }).foo.value)
})
},
'destroyAll()': function () { $assertEveryCalled (function (destroyed__2) {
var Compo = $extends (Component, {
destroy: function () { destroyed__2 () } })
var parent = new Compo ()
.attach (new Compo ())
.attach (new Compo ())
$assert (parent.attached.length === 2)
parent.destroyAll ()
parent.destroyAll ()
$assert (parent.attached.length === 0) })},
'$macroTags for component-specific macros': function () {
var Trait = $trait ({ $macroTags: {
add_2: function (def, fn, name) {
return Meta.modify (fn, function (fn) {
return fn.then (_.sum.$ (2)) }) } } })
var Base = $component ({ $macroTags: {
add_20: function (def, fn, name) {
return Meta.modify (fn, function (fn) {
return fn.then (_.sum.$ (20)) }) } } })
var Compo = $extends (Base, {
$traits: [Trait],
$macroTags: { dummy: function () {} },
testValue: $static ($add_2 ($add_20 (_.constant (20)))) })
$assert (42, Compo.testValue ())
$assertMatches (_.keys (Compo.$macroTags), ['dummy', 'add_2', 'add_20'])
_.each (_.keys (Compo.$macroTags), function (name) { delete $global['$' + name] }) },
'$raw for performance-critical methods (disables thiscall proxy)': function () {
var compo = new ($component ({
method: function (this_) { $assert (this_ === this) },
rawMethod: $raw (function (this_) { $assert (this_ !== this) }) }))
var method = compo. method; method (compo)
var rawMethod = compo.rawMethod; rawMethod (compo) },
'two-way $observable binding': function () {
var Compo = $component ({ x: $observable ('foo') })
var x = _.observable ('bar')
var compo = new Compo ({ x: x })
$assert (compo.x !== x)
$assert (compo.x.value, x.value, 'bar')
compo.x (42); $assert (x.value, 42)
x ('lol'); $assert (compo.x.value, 'lol')
/* Test unbinding */
compo.destroy ()
$assert (compo.x.queue, [])
compo.x ('yo'); $assert (x.value, 'lol') // shouldnt change
x ('oy'); $assert (compo.x.value, 'yo') // shouldnt change
},
'member order' () {
var X = $component ({
$depends: [
$trait ({ foo_1 () {} }),
$trait ({ foo_2 () {} })
],
foo_3 () {}
})
$assert (_.filter (_.keys (X.prototype), k => k.startsWith ('foo')),
['foo_1',
'foo_2',
'foo_3'])
},
/* $alias (TODO: fix bugs)
*/
/*'$alias': function () { var value = 41
var compo = $singleton (Component, {
foo: function () { return ++value },
bar: $bindable ($alias ('foo')),
baz: $memoize ($alias ('bar')) })
$assertEveryCalled (function (mkay) { compo.bar.onBefore (mkay)
$assert (compo.baz (),
compo.baz (), 42) }) },*/
/* Auto-unbinding
*/
'unbinding (simple)': function () {
var somethingHappened = _.trigger ()
var compo = $singleton (Component, { fail: function () { $fail } })
somethingHappened (compo.fail)
compo.destroy ()
somethingHappened () }, // should not invoke compo.fail
'(regression) undefined was allowed as trait': function () {
$assertThrows (function () {
var Compo = $component ({ $traits: [undefined] }) }, { message: 'invalid $traits value' }) },
'(regression) undefined members fail': function () {
var Compo = $component ({ yoba: undefined })
$assert ('yoba' in Compo.prototype) },
'(regression) $defaults with $traits fail': function () {
var Compo = $component ({ $traits: [$trait ({ $defaults: { x: 1 }})], $defaults: { a: {}, b: [], c: 0 } })
$assert (Compo.$defaults, { x: 1, a: {}, b: [], c: 0 }) },
'(regression) $defaults with $traits fail #2': function () {
var Compo = $component ({ $traits: [$trait ({ $defaults: { x: 1 }})] })
$assert (Compo.$defaults, { x: 1 }) },
'(regression) method overriding broken': function () {
var Compo = $component ({ method: function () { $fail } })
var compo = new Compo ({ value: 42, method: function () { return this.value } })
$assert (compo.method (), 42) },
'(regression) $observableProperty (false)': function () {
$assertEveryCalledOnce (function (mkay) {
$singleton (Component, {
foo: $observableProperty (false),
init: function () { this.fooChange (mkay) } }) }) },
'(regression) was not able to define inner compos at singleton compos': function () {
var Foo = $singleton (Component, {
InnerCompo: $component ({
foo: $observableProperty () }) })
var Bar = $extends (Foo.InnerCompo, { bar: $observableProperty () })
var bar = new Bar ()
$assertTypeMatches (bar, { fooChange: 'function', barChange: 'function' }) },
/*'(regression) postpone': function (testDone) { $assertEveryCalledOnce ($async (function (foo) {
$singleton (Component, {
foo: function () { foo (); },
init: function () { this.foo.postpone () } }) }), testDone) },*/
'(regression) undefined at definition': function () { $singleton (Component, { fail: undefined }) },
'(regression) properties were evaluated before init': function () {
$singleton (Component, {
fail: $property (function () { $fail }) }) },
'(regression) misinterpretation of definition': function () {
$singleton (Component, { get: function () { $fail } }) },
'(regression) alias incorrectly worked with destroy': function () {
var test = $singleton (Component, {
destroy: function () { mkay () },
close: $alias ('destroy') })
$assert (test.close, test.destroy) },
'(regression) pollution of stream listeners': function () {
var A = $trait ({ something: $bindable (function (x) { }) })
var B = $trait ({ afterSomething (x) { $assert (false) }})
var Y = $singleton (Component, { $depends: [A, B] })
var Z = $singleton (Component, { $depends: [A] })
Z.something ()
},
}
/* General syntax
*/
$global.$component = function (definition) {
return $extends (Component, definition) }
_([ 'extendable', 'trigger', 'triggerOnce', 'barrier', 'bindable', 'memoize', 'interlocked',
'memoizeCPS', 'debounce', 'throttle', 'overrideThis', 'listener', 'postpones', 'reference', 'raw', 'binds', 'observes'])
.each (Meta.globalTag)
;(function () {
var impl = function (tag, a, b) {
if (arguments.length < 3) {
const listener = $untag (a)
return _.isFunction (listener)
? Meta.setTag (tag, listener) // $observableProperty (listener)
: Meta.setTag (tag, true, a) // $observableProperty (value)
} else {
const listener = $untag (b)
return Meta.setTag (tag, _.isFunction (listener) ? listener : true, a) // $observableProperty (value, listener)
}
}
Meta.globalTag ('observableProperty', impl)
Meta.globalTag ('observable', impl)
}) ();
$global.$observableRef = function (x) { return $observableProperty ($reference (x)) }
$prototype.macro ('$depends', function (def, value, name) {
(def.$depends = $builtin ($const (_.coerceToArray (value))))
return def })
$prototype.macroTag ('extendable',
function (def, value, name) {
def[name] = $builtin ($const (value))
return def })
$global.Component = $prototype ({
$defaults: $extendable ({}),
$requires: $extendable ({}),
$macroTags: $extendable ({}),
/* Overrides default OOP.js implementation
*/
$impl: {
sequence: function (def, base) { return _.sequence (
this.convertPropertyAccessors,
this.extendWithTags,
this.flatten,
this.generateCustomCompilerImpl (base),
this.ensureFinalContracts (base),
this.generateConstructor (base),
this.evalAlwaysTriggeredMacros (base),
this.evalMemberTriggeredMacros (base),
this.expandTraitsDependencies,
this.mergeExtendables (base),
this.contributeTraits (base),
this.mergeStreams,
this.mergeBindables,
this.generateBuiltInMembers (base),
this.callStaticConstructor,
this.expandAliases,
this.groupMembersByTagForFastEnumeration,
this.defineStaticMembers,
this.defineInstanceMembers) },
expandTraitsDependencies: function (def) {
if (_.isNonempty ($untag (def.$depends)) &&
_.isEmpty ($untag (def.$traits))) {
def.$traits = DAG.sortedSubgraphOf (def, {
nodes: function (def) {
return $untag (def.$depends) } }) }; return def },
mergeExtendables: function (base) { return function (def) {
_.each (base.$definition, function (value, name) {
if ($extendable.is (value)) {
def[name] = Meta.modify ( value,
function ( value) {
value = _.extendedDeep (value, $untag (def[name] || {}))
_.each ($untag (def.$traits),
function (trait) { if (!trait) { log.e (def.$traits)
throw new Error ('invalid $traits value') }
var traitVal = trait.$definition [name]
if (traitVal) { value = _.extendedDeep ($untag (traitVal), value) } })
return value }) } });
return def } },
mergeTraitsMembers: function (def, traits) {
var newDef = {} // clone to re-add members in correct order (traits first)
var pool = {}, bindables = {}, streams = {}
var macroTags = $untag (def.$macroTags)
var definitions = _.pluck (traits, '$definition').concat (_.clone (def))
_.each (definitions, function (traitDef) {
_.each ((macroTags && this.applyMacroTags (macroTags,
_.extend (_.clone (traitDef), {
constructor: def.constructor }))) || traitDef,
function (member, name) {
if ($builtin.isNot (member) &&
$builtin.isNot (def[name]) && (name !== 'constructor')) {
if ($bindable.is (member)) { bindables[name] = member }
if (Component.isStreamDefinition (member)) { streams[name] = member }
(pool[name] || (pool[name] = [])).push (member); newDef[name] = member } }) }, this)
def.__bindables = bindables
def.__streams = streams
def.__membersByName = pool
/* Re-add members in correct order */
for (const k of Object.keys (newDef)) {
delete def[k]
}
for (const k of Object.keys (newDef)) {
def[k] = newDef[k]
}
},
mergeStreams: function (def) { var pool = def.__membersByName
_.each (def.__streams, function (stream, name) {
var clonedStream = def[name] = Meta.new (stream)
clonedStream.listeners = []
_.each (pool[name], function (member) {
if (member !== stream) {
clonedStream.listeners.push ($untag (member)) } }) }); return def },
mergeBindables: function (def) { var pool = def.__membersByName
_.each (def.__bindables, (member, name) => {
var bound = _.filter2 (_.bindable.hooks, function (hook, i) {
var bound = pool[_.bindable.hooksShort[i] + name.capitalized]
return bound ? [hook, bound] : false })
if (bound.length) { var hooks = {}
_.each (bound, function (kv) {
_.each (kv[1], function (fn) { fn = $untag (fn)
if (_.isFunction (fn)) { var k = '_' + kv[0]; (hooks[k] || (hooks[k] = [])).push (fn) } }) })
def[name] = $bindable ({ hooks: hooks }, member)
}
})
return def
}
},
/* Syntax helper
*/
isStreamDefinition: $static (function (def) {
const tags = Meta.tags (def)
return tags.trigger || tags.triggerOnce || tags.barrier || tags.observable || tags.observableProperty }),
/* Another helper (it was needed because _.methods actually evaluate $property values while enumerating keys,
and it ruins most of application code, because it happens before Component is actually created).
*/
mapMethods: function (/* [predicate, ] iterator */) { var iterator = _.last (arguments),
predicate = (arguments.length === 1 ? _.constant (true) : arguments[0])
var methods = []
for (var k in this) {
var def = this.constructor.$definition[k]
if ($property.isNot (def)) { var fn = this[k]
if (_.isFunction (fn) && !_.isPrototypeConstructor (fn) && predicate (def)) {
this[k] = iterator.call (this, fn, k, def) || fn } } } },
enumMethods: function (_1, _2) {
if (arguments.length === 2) { this.mapMethods (_1, _2.returns (undefined)) }
else { this.mapMethods ( _1.returns (undefined)) } },
/* Thou shall not override this
*/
constructor: $final (function (arg1, arg2) {
this.parent_ = undefined
this.children_ = []
var cfg = this.cfg = ((typeof arg1 === 'object') ? arg1 : {}),
componentDefinition = this.constructor.$definition
/* Apply $defaults
*/
if (this.constructor.$defaults) {
cfg = this.cfg = _.extend (_.cloneDeep (this.constructor.$defaults), cfg) }
/* Add thiscall semantics to methods
*/
this.mapMethods (function (fn, name, def) {
if ((name !== '$') && (name !== 'init') && $raw.isNot (def)) { return this.$ (fn) }
})
/* Listen self destroy method
*/
_.onBefore (this, 'destroy', this._beforeDestroy)
_.onAfter (this, 'destroy', this._afterDestroy)
var initialStreamListeners = []
var excludeFromCfg = { init: true }
/* Expand macros
TODO: execute this substitution at $prototype code-gen level, not at instance level
*/
_.each (componentDefinition, function (def, name) { if (def !== undefined) {
const member = Meta.unwrap (def)
const tags = Meta.tags (def)
/* Expand $observableProperty
TODO: rewrite with $prototype.macro
*/
if (tags.observableProperty) { var definitionValue = member
var defaultValue = (name in cfg) ? cfg[name] : definitionValue
var streamName = name + 'Change'
/* xxxChange stream
*/
var observable = excludeFromCfg[streamName] = this[streamName] = _.observable ()
observable.context = this
observable.postpones = tags.postpones
/* auto-coercion of incoming values to prototype instance
*/
if (_.isPrototypeInstance (definitionValue)) { var constructor = definitionValue.constructor
observable.beforeWrite = function (value) {
return constructor.isTypeOf (value) ? value : (new constructor (value)) } }
/* tracking by reference
*/
if (tags.reference) {
observable.trackReference = true }
/* property
*/
_.defineProperty (this, name, {
get: function () { return observable.value },
set: function (x) { observable.write.call (this, x) } })
/* Default listeners (come from traits)
*/
if (def.listeners) {
_.each (def.listeners, function (value) {
initialStreamListeners.push ([observable, value]) }) }
/* Default listener which comes from $observableProperty (defValue, defListener) syntax
*/
if (_.isFunction (tags.observableProperty)) {
initialStreamListeners.push ([observable, tags.observableProperty]) }
/* write default value
*/
if (defaultValue !== undefined) {
observable (defaultValue) } }
/* Expand streams
*/
else if (Component.isStreamDefinition (def)) {
var stream = excludeFromCfg[name] = this[name] = _.extend (
(tags.trigger ? _.trigger :
(tags.triggerOnce ? _.triggerOnce :
(tags.observable ? _.observable :
(tags.barrier ? _.barrier : undefined)))) (member), { context: this, postpones: tags.postpones })
/* tracking by reference
*/
if (tags.reference) {
observable.trackReference = true }
if (def.listeners) {
_.each (def.listeners, function (value) {
initialStreamListeners.push ([stream, value]) }) }
/* Default listener which comes from $observable (defValue, defListener) syntax
*/
if (_.isFunction (tags.observable)) {
initialStreamListeners.push ([stream, tags.observable]) }
var defaultListener = cfg[name]
if (defaultListener) {
if (tags.observable && defaultListener.isObservable) { // two-way observable binding
defaultListener.tie (stream) }
else {
initialStreamListeners.push ([stream, defaultListener]) } } }
/* Expand $listener (TODO: REMOVE)
*/
if (tags.listener) {
this[name].queuedBy = [] }
/* Expand $interlocked
*/
if (tags.interlocked) {
this[name] = _.interlocked (this[name]) }
/* Expand $bindable
*/
if (tags.bindable) {
this[name] = _.extend (_.bindable (this[name], this),
_.map2 (tags.bindable.hooks || {}, hooks => _.map (hooks, f => this.$ (f)))) }
/* Expand $debounce
*/
if (tags.debounce) { var fn = this[name], opts = _.coerceToObject (tags.debounce)
this[name] = fn.debounced (opts.wait || 500, opts.immediate) }
/* Expand $throttle
*/
if (tags.throttle) { var fn = this[name], opts = _.coerceToObject (tags.throttle)
this[name] = _.throttle (fn, opts.wait || 500, opts) }
/* Expand $memoize
*/
if (tags.memoize) {
this[name] = _.memoize (this[name]) }
else if (tags.memoizeCPS) {
this[name] = _.cps.memoize (this[name]) } } }, this)
/* Add before/after stage to init
*/
var init = this.init
this.init = this._beforeInit
.then (init
.then (this._afterInit)).bind (this)
/* Apply cfg thing
*/
_.each (cfg, function (value, name) {
if (!(name in excludeFromCfg)) {
this[name] = _.isFunction (value) ? this.$ (value) : value } }, this)
/* Fixup aliases (they're now pointing to nothing probably, considering what we've done at this point)
*/
_.each (componentDefinition, function (def, name) {
if ($alias.is (def) && $raw.isNot (def)) {
this[name] = this[$untag (def)] } }, this)
/* Check $overrideThis
*/
/*_.each (componentDefinition, function (def, name) {
if (def.$overrideThis && this[name] === undefined) {
throw new Error (name + ' should be overriden') } })*/
/* Check $requires (TODO: make human-readable error reporting)
*/
if (_.hasAsserts) {
_.each (this.constructor.$requires, function (contract, name) {
$assertTypeMatches (_.fromPairs ([[name, this[name]]]),
_.fromPairs ([[name, contract]])) }, this) }
/* Subscribe default listeners
*/
_.each (initialStreamListeners, function (v) { v[0].call (this, v[1]) }, this)
/* Call init (if not marked as deferred)
*/
if (!(cfg.init === false || (this.constructor.$defaults && (this.constructor.$defaults.init === false)))) {
var result = this.init ()
if (result instanceof Promise) {
result.panic } } }),
/* Arranges methods defined in $traits in chains and evals them
*/
methodChain (name, { reverse = false, until = () => false } = {}) {
const methods = _.filter2 (this.constructor.$traits || [], Trait => {
const method = Trait.prototype[name]
return (method && method.bind (this)) || false })
return (...args) => __.each (reverse ? methods.reverse () : methods, (fn, i, break_) => (
__.then (fn (...args), returnValue => {
if (until (returnValue)) {
break_ ()
}
})
))
},
/* LEGACY
TODO: find why methodChain () does not work as a replacement */
callChainMethod: function (name) { var self = this
//console.log ('callChainMethod', this.constructor.$meta.name, name)
const methods = _.filter2 (this.constructor.$traits || [], function (Trait) {
var method = Trait.prototype[name]
// if (method) {
// return (...args) => {
// console.log ('Calling', Trait.$meta.name, name)
// return method.call (self, ...args)
// }
// }
return (method && method.bind (self)) || false })
return __.seq (methods) },
/* Lifecycle
*/
_beforeInit: function () {
if (this.initialized.already) {
throw new Error ('Component: I am already initialized. Probably you\'re doing it wrong.') }
return this.callChainMethod ('beforeInit') },
init: function () { /* return Promise for asynchronous init */ },
_afterInit: function () { var cfg = this.cfg,
self = this
return __.then (this.callChainMethod.$ ('afterInit'), function () {
self.initialized (true)
self.alive (true)
/* Bind default property listeners. Doing this after init, because property listeners
get called immediately after bind (observable semantics), and we're want to make
sure that component is initialized at the moment of call.
We do not do this for other streams, as their execution is up to component logic,
and they're might get called at init, so their default values get bound before init.
*/
_.each (self.constructor.$definition, function (def, name) {
if ($observableProperty.is (def)) {
name += 'Change'
var defaultListener = cfg[name]
if (defaultListener) {
self[name] (defaultListener) } } })
return true }) },
initialized: $barrier (),
alive: $observable (false),
_beforeDestroy: function () {
if (this.destroyed_) {
throw new Error ('Component: I am already destroyed. Probably you\'re doing it wrong.') }
if (this.destroying_) {
throw new Error ('Component: Recursive destroy() call detected. Probably you\'re doing it wrong.') }
this.destroying_ = true
_.each (this.constructor.$traits, function (Trait) {
if (Trait.prototype.beforeDestroy) {
Trait.prototype.beforeDestroy.call (this) } }, this)
this.alive (false)
/* Unbind streams
*/
this.enumMethods (_.off.arity1)
/* Destroy children
*/
_.each (this.children_, _.method ('destroy'))
this.children_ = [] },
destroy: function () {},
_afterDestroy: function () {
_.each (this.constructor.$traits, function (Trait) {
if (Trait.prototype.destroy) {
Trait.prototype.destroy.call (this) }
if (Trait.prototype.afterDestroy) {
Trait.prototype.afterDestroy.call (this) } }, this)
delete this.destroying_
this.parent_ = undefined
this.destroyed_ = true },
/* Parent manip.
*/
attachedTo: $property (function () {
return this.parent_ }),
attachTo: function (p) {
if (p === this) {
throw new Error ('smells like time-travel paradox.. how else can I be parent of myself?') }
if (this.parent_ !== p) {
if ((this.parent_) !== undefined) {
this.parent_.children_.remove (this) }
if ((this.parent_ = p) !== undefined) {
this.parent_.children_.push (this) }} return this },
detach: function () {
return this.attachTo (undefined) },
/* Child manip.
*/
attached: $property (function () {
return this.children