git ssb

1+

Matt McKegg / mutant



Tree: d21f00b20866259a0cc392aac18a581d68aa23be

Files: d21f00b20866259a0cc392aac18a581d68aa23be / computed.js

6323 bytesRaw
1/* A lazy binding take on computed */
2// - doesn't start watching observables until itself is watched, and then releases if unwatched
3// - avoids memory/watcher leakage
4// - attaches to inner observables if these are returned from value
5// - doesn't broadcast if value is same as last value (and is `value type` or observable - can't make assuptions about reference types)
6// - doesn't broadcast if value is computed.NO_CHANGE
7
8var resolve = require('./resolve')
9var isObservable = require('./is-observable')
10var isSame = require('./lib/is-same')
11var onceIdle = require('./once-idle')
12
13module.exports = computed
14
15computed.NO_CHANGE = {}
16computed.extended = extendedComputed
17
18function computed (observables, lambda, opts) {
19 // opts: nextTick, comparer, context, passthru
20 var instance = new ProtoComputed(observables, lambda, opts)
21 return instance.MutantComputed.bind(instance)
22}
23
24// optimise memory usage
25function ProtoComputed (observables, lambda, opts) {
26 if (!Array.isArray(observables)) {
27 observables = [observables]
28 }
29 this.values = []
30 this.releases = []
31 this.computedValue = null
32 this.outputValue = null
33 this.inner = null
34 this.updating = false
35 this.live = false
36 this.initialized = false
37 this.listeners = []
38 this.observables = observables
39 this.lambda = lambda
40 this.opts = opts
41 this.comparer = opts && opts.comparer || null
42
43 // when true, don't expand nested observables, just treat as values
44 this.passthru = opts && opts.passthru || null
45
46 this.context = opts && opts.context || {}
47 this.boundOnUpdate = this.onUpdate.bind(this)
48 this.boundUpdateNow = this.updateNow.bind(this)
49 this.boundUnlisten = this.unlisten.bind(this)
50}
51
52ProtoComputed.prototype = {
53 MutantComputed: function (listener) {
54 if (!listener) {
55 return this.getValue()
56 }
57
58 if (typeof listener !== 'function') {
59 throw new Error('Listeners must be functions.')
60 }
61
62 this.listeners.push(listener)
63 this.listen()
64
65 return this.removeListener.bind(this, listener)
66 },
67 removeListener: function (listener) {
68 for (var i = 0, len = this.listeners.length; i < len; i++) {
69 if (this.listeners[i] === listener) {
70 this.listeners.splice(i, 1)
71 break
72 }
73 }
74 if (!this.listeners.length) {
75 this.unlisten()
76 }
77 },
78 listen: function () {
79 if (!this.live) {
80 for (var i = 0, len = this.observables.length; i < len; i++) {
81 if (isObservable(this.observables[i])) {
82 this.releases.push(this.observables[i](this.boundOnUpdate))
83 }
84 }
85 if (this.inner) {
86 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
87 }
88
89 this.live = true
90
91 if (!this.update() && this.inner) {
92 // no change, but make sure that inner value is up to date
93 this.onInnerUpdate(this.inner, resolve(this.inner))
94 }
95
96 if (this.opts && this.opts.onListen) {
97 var release = this.opts.onListen()
98 if (typeof release === 'function') {
99 this.releases.push(release)
100 }
101 }
102 }
103 },
104 unlisten: function () {
105 if (this.live && !this.listeners.length) {
106 this.live = false
107
108 if (this.releaseInner) {
109 this.releaseInner()
110 this.releaseInner = null
111 }
112
113 while (this.releases.length) {
114 this.releases.pop()()
115 }
116
117 if (this.opts && this.opts.onUnlisten) {
118 this.opts.onUnlisten()
119 }
120 }
121 },
122 update: function () {
123 var changed = false
124 for (var i = 0, len = this.observables.length; i < len; i++) {
125 var newValue = resolve(this.observables[i])
126 if (!isSame(newValue, this.values[i], this.comparer)) {
127 changed = true
128 this.values[i] = newValue
129 }
130 }
131
132 if (changed || !this.initialized) {
133 this.initialized = true
134 var newComputedValue = this.lambda.apply(this.context, this.values)
135
136 if (newComputedValue === computed.NO_CHANGE) {
137 return false
138 }
139
140 if (!isSame(newComputedValue, this.computedValue, this.comparer)) {
141 if (this.releaseInner) {
142 this.releaseInner()
143 this.inner = this.releaseInner = null
144 }
145
146 this.computedValue = newComputedValue
147
148 if (isObservable(newComputedValue) && !this.passthru) {
149 // handle returning observable from computed
150 this.outputValue = newComputedValue()
151 this.inner = newComputedValue
152 if (this.live) {
153 this.releaseInner = this.inner(this.onInnerUpdate.bind(this, this.inner))
154 }
155 } else {
156 this.outputValue = this.computedValue
157 }
158 return true
159 }
160 }
161 return false
162 },
163 onUpdate: function () {
164 if (this.opts && this.opts.idle) {
165 if (!this.updating) {
166 this.updating = true
167 onceIdle(this.boundUpdateNow)
168 }
169 } else if (this.opts && this.opts.nextTick) {
170 if (!this.updating) {
171 this.updating = true
172 setImmediate(this.boundUpdateNow)
173 }
174 } else {
175 this.updateNow()
176 }
177 },
178 onInnerUpdate: function (obs, value) {
179 if (obs === this.inner) {
180 if (!isSame(value, this.outputValue, this.comparer)) {
181 this.outputValue = value
182 this.broadcast()
183 }
184 }
185 },
186 updateNow: function () {
187 this.updating = false
188 if (this.update()) {
189 this.broadcast()
190 }
191 },
192 getValue: function () {
193 if (!this.updating && !this.live) {
194 // temporarily become live to watch for changes until next cycle to stop
195 // potential double refresh and handle weird race conditions
196 this.listen() // triggers update
197 setImmediate(this.boundUnlisten) // only runs if no listeners have been added
198 }
199 return this.outputValue
200 },
201 broadcast: function () {
202 // cache listeners in case modified during broadcast
203 var listeners = this.listeners.slice(0)
204 for (var i = 0, len = listeners.length; i < len; i++) {
205 listeners[i](this.outputValue)
206 }
207 }
208}
209
210function extendedComputed (observables, update) {
211 var live = false
212
213 var instance = computed(observables, function () {
214 return update()
215 }, {
216 onListen: function () { live = true },
217 onUnlisten: function () { live = false }
218 })
219
220 instance.checkUpdated = function () {
221 if (!live) {
222 update()
223 }
224 }
225
226 return instance
227}
228

Built with git-ssb-web