1 | var resolve = require('./resolve')
|
2 | var LazyWatcher = require('./lib/lazy-watcher')
|
3 | var isSame = require('./lib/is-same')
|
4 | var addCollectionMethods = require('./lib/add-collection-methods')
|
5 | var onceIdle = require('./once-idle')
|
6 |
|
7 | module.exports = Map
|
8 |
|
9 | function Map (obs, lambda, opts) {
|
10 |
|
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 |
|
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 |
|
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 |
|
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 |
|
280 | function 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 |
|
292 | function 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 |
|
306 | function 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 |
|
320 | function invokeRelease (item) {
|
321 | if (item.release) {
|
322 | item.release()
|
323 | item.release = null
|
324 | }
|
325 | }
|