forked from DanielXMoore/Observable
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.coffee
268 lines (210 loc) · 6.97 KB
/
main.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
###
Observable
==========
`Observable` allows for observing arrays, functions, and objects.
Function dependencies are automagically observed.
Standard array methods are proxied through to the underlying array.
###
"use strict"
module.exports = Observable = (value, context) ->
# Return the object if it is already an observable object.
return value if typeof value?.observe is "function"
# Maintain a set of listeners to observe changes and provide a helper to notify each observer.
listeners = []
notify = (newValue) ->
copy(listeners).forEach (listener) ->
listener(newValue)
# If `value` is a function compute dependencies and listen to observables that it depends on.
if typeof value is 'function'
fn = value
# Our return function is a function that holds only a cached value which is updated when it's dependencies change.
# The `magicDependency` call is so other functions can depend on this computed function the same way we depend on other types of observables.
self = ->
# Automagic dependency observation
magicDependency(self)
return value
# We expose releaseDependencies so that
self.releaseDependencies = ->
self._observableDependencies?.forEach (observable) ->
observable.stopObserving changed
# We need to recompute our dependencies whenever any observable value that our function depends on changes. We keep a set
# of observables (so we don't needlessly recompute the same ones multiple times). When a dependency changes we recompute
# the new set of dependencies and unsubscribe from the old set.
changed = ->
observableDependencies = new Set
value = tryCallWithFinallyPop observableDependencies, fn, context
self.releaseDependencies()
self._observableDependencies = observableDependencies
observableDependencies.forEach (observable) ->
observable.observe changed
notify(value)
changed()
else
# When called with zero arguments it is treated as a getter. When called with one argument it is treated as a setter.
# Changes to the value will trigger notifications. The value is always returned.
self = (newValue) ->
if arguments.length > 0
if value != newValue
value = newValue
notify(newValue)
else
# Automagic dependency observation
magicDependency(self)
return value
# Non-computed observables have no dependencies, releasing them is a non-operation.
self.releaseDependencies = noop
# If the value is an array then proxy array methods and add notifications to mutation events.
if Array.isArray(value)
[
"concat"
"every"
"filter"
"forEach"
"indexOf"
"join"
"lastIndexOf"
"map"
"reduce"
"reduceRight"
"slice"
"some"
].forEach (method) ->
self[method] = (args...) ->
magicDependency(self)
value[method](args...)
[
"pop"
"push"
"reverse"
"shift"
"splice"
"sort"
"unshift"
].forEach (method) ->
self[method] = (args...) ->
returnValue = value[method](args...)
notify(value)
return returnValue
# Provide length on a best effort basis because older browsers choke
if PROXY_LENGTH
Object.defineProperty self, 'length',
get: ->
magicDependency(self)
value.length
set: (length) ->
returnValue = value.length = length
notify(value)
return returnValue
# Extra methods for array observables
extend self,
# Remove an element from the array and notify observers of changes.
remove: (object) ->
index = value.indexOf(object)
if index >= 0
returnValue = value.splice(index, 1)[0]
notify(value)
return returnValue
get: (index) ->
magicDependency(self)
value[index]
first: ->
magicDependency(self)
value[0]
last: ->
magicDependency(self)
value[value.length-1]
size: ->
magicDependency(self)
value.length
if Object::toString.call(value) is '[object Object]'
# proxy object properties and add notifications to mutation events
defProp = (property) ->
Object.defineProperty self, property,
get: ->
magicDependency(self)
# if object property is Observable, e.g obj = Observable { a: Observable 1 }
if typeof value[property]?.observe is "function"
value[property]()
else
value[property]
set: (val) ->
# if object property is Observable, e.g obj = Observable { a: Observable 1 }
if typeof value[property]?.observe is "function"
value[property] val
else
value[property] = val
notify value
defProp prop for own prop of value
# Proxy object methods, e.g. Object.keys(obj)
[
"keys"
"values"
"entries"
].forEach (method) ->
Object.defineProperty self, method,
get: ->
magicDependency(self)
Object[method] value
# Extra methods for object observables
extend self,
# Remove an element from the object and notify observers of changes.
remove: (object) ->
if returnValue = value[object]
delete value[object]
notify(value)
return returnValue
extend: (obj) ->
magicDependency(self)
value = Object.assign {}, value, obj
defProp prop for own prop of obj
notify value
# alias
self.assign = self.extend
extend self,
listeners: listeners
observe: (listener) ->
listeners.push listener
stopObserving: (fn) ->
remove listeners, fn
toggle: ->
self !value
increment: (n=1) ->
self value + n
decrement: (n=1) ->
self value - n
toString: ->
"Observable(#{value})"
return self
# Appendix
# --------
extend = Object.assign
# Super hax for computing dependencies. This needs to be a shared global so that different bundled versions of observable libraries can interoperate.
global.OBSERVABLE_ROOT_HACK = []
magicDependency = (self) ->
observerSet = last(global.OBSERVABLE_ROOT_HACK)
if observerSet
observerSet.add self
# Optimization: Keep the function containing the try-catch as small as possible.
tryCallWithFinallyPop = (observableDependencies, fn, context) ->
global.OBSERVABLE_ROOT_HACK.push(observableDependencies)
try
fn.call(context)
finally
global.OBSERVABLE_ROOT_HACK.pop()
remove = (array, value) ->
index = array.indexOf(value)
if index >= 0
array.splice(index, 1)[0]
copy = (array) ->
array.concat([])
last = (array) ->
array[array.length - 1]
noop = ->
# Check if we can proxy function length property.
try
Object.defineProperty (->), 'length',
get: noop
set: noop
PROXY_LENGTH = true
catch
PROXY_LENGTH = false