UNPKG

7.48 kBJavaScriptView Raw
1var resolve = require('./resolve')
2var LazyWatcher = require('./lib/lazy-watcher')
3var isSame = require('./lib/is-same')
4var addCollectionMethods = require('./lib/add-collection-methods')
5var onceIdle = require('./once-idle')
6
7module.exports = Map
8
9function Map (obs, lambda, opts) {
10 // opts: comparer, maxTime, onRemove
11
12 if (typeof lambda !== 'function') throw new Error('mutant/map lambda must be a function')
13
14 var comparer = opts && opts.comparer || null
15 var releases = []
16 var binder = LazyWatcher(update, listen, unlisten)
17
18 if (opts && opts.nextTick) binder.nextTick = true
19 if (opts && opts.idle) binder.idle = true
20
21 var itemInvalidators = new global.Map()
22 var lastValues = new global.Map()
23 var rawSet = new global.Set()
24
25 var items = []
26
27 var raw = []
28 var values = []
29 var watches = []
30
31 binder.value = values
32
33 // incremental update
34 var queue = []
35 var maxTime = null
36 if (opts && opts.maxTime) {
37 maxTime = opts.maxTime
38 }
39
40 var result = function MutantMap (listener) {
41 if (!listener) {
42 return binder.getValue()
43 }
44 return binder.addListener(listener)
45 }
46
47 addCollectionMethods(result, raw, binder.checkUpdated)
48
49 return result
50
51 // scoped
52
53 function listen () {
54 if (typeof obs === 'function') {
55 releases.push(obs(binder.onUpdate))
56 }
57 rebindAll()
58
59 Array.from(itemInvalidators.values()).forEach(function (invalidators) {
60 invalidators.forEach(function (invalidator) {
61 invalidator.release = invalidator.observable(invalidate.bind(null, invalidator))
62 })
63 })
64
65 if (opts && opts.onListen) {
66 var release = opts.onListen()
67 if (typeof release === 'function') {
68 releases.push(release)
69 }
70 }
71 }
72
73 function unlisten () {
74 while (releases.length) {
75 releases.pop()()
76 }
77 rebindAll()
78
79 Array.from(itemInvalidators.values()).forEach(function (invalidators) {
80 invalidators.forEach(invokeRelease)
81 })
82
83 if (opts && opts.onUnlisten) {
84 opts.onUnlisten()
85 }
86 }
87
88 function update () {
89 var changed = false
90
91 if (items.length !== getLength(obs)) {
92 changed = true
93 }
94
95 var startedAt = Date.now()
96
97 for (var i = 0, len = getLength(obs); i < len; i++) {
98 var item = get(obs, i)
99 var currentItem = items[i]
100 items[i] = item
101
102 if (!isSame(item, currentItem, comparer) || (!binder.live && checkInvalidated(item))) {
103 if (maxTime && Date.now() - startedAt > maxTime) {
104 queueUpdateItem(i)
105 } else {
106 updateItem(i)
107 }
108 changed = true
109 }
110 }
111
112 if (changed) {
113 // clean up cache
114 var oldLength = raw.length
115 var newLength = getLength(obs)
116 Array.from(lastValues.keys()).filter(notIncluded, obs).forEach(removeItem)
117 items.length = newLength
118 values.length = newLength
119 raw.length = newLength
120 for (var index = newLength; index < oldLength; index++) {
121 rebind(index)
122 }
123 Array.from(rawSet.values()).filter(notIncluded, raw).forEach(removeMapped)
124 }
125
126 return changed
127 }
128
129 function checkInvalidated (item) {
130 if (itemInvalidators.has(item)) {
131 return itemInvalidators.get(item).some(function (invalidator) {
132 lastValues.delete(invalidator.item)
133 return !isSame(invalidator.currentValue, resolve(invalidator.observable), comparer)
134 })
135 }
136 }
137
138 function queueUpdateItem (i) {
139 if (!queue.length) {
140 doSoon(flushQueue)
141 }
142 if (!~queue.indexOf(i)) {
143 queue.push(i)
144 }
145 }
146
147 function flushQueue () {
148 var startedAt = Date.now()
149 while (queue.length && (!maxTime || Date.now() - startedAt < maxTime)) {
150 updateItem(queue.shift())
151 }
152 binder.broadcast()
153 if (queue.length) {
154 doSoon(flushQueue)
155 }
156 }
157
158 function invalidateOn (item, obs) {
159 if (!itemInvalidators.has(item)) {
160 itemInvalidators.set(item, [])
161 }
162
163 var invalidators = itemInvalidators.get(item)
164 var invalidator = {
165 currentValue: resolve(obs),
166 observable: obs,
167 item: item,
168 release: null
169 }
170
171 invalidators.push(invalidator)
172
173 if (binder.live) {
174 invalidator.release = invalidator.observable(invalidate.bind(null, invalidator))
175 }
176 }
177
178 function addInvalidateCallback (item) {
179 return invalidateOn.bind(null, item)
180 }
181
182 function removeItem (item) {
183 lastValues.delete(item)
184 if (itemInvalidators.has(item)) {
185 itemInvalidators.get(item).forEach(invokeRelease)
186 itemInvalidators.delete(item)
187 }
188 }
189
190 function removeMapped (mappedItem) {
191 rawSet.delete(mappedItem)
192 if (opts && opts.onRemove) {
193 opts.onRemove(mappedItem)
194 }
195 }
196
197 function invalidate (entry) {
198 var changed = []
199 var length = getLength(obs)
200 lastValues.delete(entry.item)
201 for (var i = 0; i < length; i++) {
202 if (get(obs, i) === entry.item) {
203 changed.push(i)
204 }
205 }
206 if (changed.length) {
207 var rawValue = raw[changed[0]]
208 changed.forEach(function (index) {
209 raw[index] = null
210 })
211 if (!raw.includes(rawValue)) {
212 removeMapped(rawValue)
213 }
214 changed.forEach(updateItem)
215 binder.broadcast()
216 }
217 }
218
219 function updateItem (i) {
220 if (i < getLength(obs)) {
221 var item = get(obs, i)
222 if (!lastValues.has(item) || !isSame(item, item, comparer)) {
223 if (itemInvalidators.has(item)) {
224 itemInvalidators.get(item).forEach(invokeRelease)
225 itemInvalidators.delete(item)
226 }
227 var newValue = lambda(item, addInvalidateCallback(item))
228 if (newValue !== raw[i]) {
229 raw[i] = newValue
230 }
231 rawSet.add(newValue)
232 lastValues.set(item, raw[i])
233 } else {
234 raw[i] = lastValues.get(item)
235 }
236 rebind(i)
237 values[i] = resolve(raw[i])
238 }
239 }
240
241 function rebind (index) {
242 if (watches[index]) {
243 watches[index]()
244 watches[index] = null
245 }
246
247 if (binder.live) {
248 if (typeof raw[index] === 'function') {
249 watches[index] = updateValue(raw[index], index)
250 }
251 }
252 }
253
254 function rebindAll () {
255 for (var i = 0; i < raw.length; i++) {
256 rebind(i)
257 }
258 }
259
260 function updateValue (obs, index) {
261 return obs(function (value) {
262 if (!isSame(values[index], value, comparer)) {
263 values[index] = value
264 binder.broadcast()
265 }
266 })
267 }
268
269 function doSoon (fn) {
270 if (opts.idle) {
271 onceIdle(fn)
272 } else if (opts.delayTime) {
273 setTimeout(fn, opts.delayTime)
274 } else {
275 setImmediate(fn)
276 }
277 }
278}
279
280function get (target, index) {
281 if (typeof target === 'function' && !target.get) {
282 target = target()
283 }
284
285 if (Array.isArray(target)) {
286 return target[index]
287 } else if (target && target.get) {
288 return target.get(index)
289 }
290}
291
292function getLength (target) {
293 if (typeof target === 'function' && !target.getLength) {
294 target = target()
295 }
296
297 if (Array.isArray(target)) {
298 return target.length
299 } else if (target && target.get) {
300 return target.getLength()
301 }
302
303 return 0
304}
305
306function notIncluded (value) {
307 if (this.includes) {
308 return !this.includes(value)
309 } else if (this.indexOf) {
310 return !~this.indexOf(value)
311 } else if (typeof this === 'function') {
312 var array = this()
313 if (array && array.includes) {
314 return !array.includes(value)
315 }
316 }
317 return true
318}
319
320function invokeRelease (item) {
321 if (item.release) {
322 item.release()
323 item.release = null
324 }
325}