weak_observable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,5 @@
1
+ --color
2
+ -fp
3
+ -Ilib
4
+ -Ispec
5
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ # [v1.0.0][]
2
+
3
+ Initial release. WeakObservable and Hub!
4
+
5
+ [v1.0.0]: https://github.com/Burgestrand/weak_observable/tree/v1.0.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+ gemspec
3
+ gem 'yard'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Kim Burgestrand
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,67 @@
1
+ # WeakObservable
2
+
3
+ WeakObservable is very similar to Observable from ruby’ standard library, but
4
+ with the very important difference in that it allows it’s subscribers to be
5
+ garbage collected.
6
+
7
+ ## Usage
8
+
9
+ First, install the gems with rubygems.
10
+
11
+ ```shell
12
+ gem install weak_observable
13
+ ```
14
+
15
+ Now include it onto any object, set up your subscribers and play away!
16
+
17
+ ```ruby
18
+ require 'weak_observable'
19
+
20
+ class Observer
21
+ def update(event, *args, &block)
22
+ puts "[#{event}]: #{args.inspect}"
23
+ end
24
+ end
25
+
26
+ class Playlist
27
+ include WeakObservable::Mixin
28
+ end
29
+
30
+ # Create an observer to wait for notifications.
31
+ observer = Observer.new
32
+
33
+ playlist = Playlist.new
34
+ playlist.observers << observer
35
+ playlist.observers.count # => 1
36
+
37
+ # Notify all observers. Any arguments and given block goes out to them all.
38
+ playlist.observers.notify(:ping)
39
+ # ^ the above ends up calling observer.update(:ping)
40
+
41
+ # Unassign the variable, allowing the observer to be garbage collected.
42
+ observer = nil
43
+
44
+ # Some time passes, ruby garbage collection eventually kicks in, and now…
45
+ playlist.observers.count # => 0
46
+ playlist.observers.notify(:ping) # nothing happens, we have no observers.
47
+ ```
48
+
49
+ ## Contributing
50
+
51
+ Please fork the repository and clone it. To get started with development you’ll
52
+ need to install the development dependencies and make sure you can run the
53
+ tests. You can easily install all development dependencies using bundler.
54
+
55
+ ```shell
56
+ $> gem install bundler
57
+ $> bundle install # => installs development dependencies
58
+ ```
59
+
60
+ Once you have the dependencies installed you should be able to run the tests.
61
+
62
+ ```shell
63
+ $> rake
64
+ ```
65
+
66
+ They should be green. Now go ahead and add your tests, add your changes, and push
67
+ it back up to GitHub. Once you feel you’re done create a pull request. Thank you!
@@ -0,0 +1,15 @@
1
+ begin
2
+ require "bundler/gem_tasks"
3
+ rescue LoadError
4
+ end
5
+
6
+ begin
7
+ require 'yard'
8
+ require 'yard/rake/yardoc_task'
9
+ YARD::Rake::YardocTask.new
10
+ end
11
+
12
+ require 'rspec/core/rake_task'
13
+ RSpec::Core::RakeTask.new
14
+
15
+ task :default => :spec
@@ -0,0 +1,89 @@
1
+ require "ref"
2
+ require "weak_observable/version"
3
+ require "weak_observable/mixin"
4
+ require "weak_observable/hub"
5
+
6
+ # WeakObservable is like the Observable from standard library,
7
+ # with a slightly different API and one important difference:
8
+ # it does not keep any strong references to it’s observers.
9
+ #
10
+ # This is useful in case you want observers, but don’t really
11
+ # care if they are garbage collected or not. Mostly useful for
12
+ # asynchronous callbacks in C extensions or FFI bindings.
13
+ #
14
+ # @example
15
+ # observable = WeakObservable.new
16
+ # observer = Object.new
17
+ #
18
+ # observable.add(observer)
19
+ # observable.notify # notifies observable
20
+ #
21
+ # observer = nil
22
+ # GC.start # this might garbage collect observer
23
+ # observable.notify # if observer is garbage collected, this does nothing now
24
+ class WeakObservable
25
+ def initialize
26
+ @monitor = Ref::SafeMonitor.new
27
+ @observers = Ref::WeakValueMap.new
28
+ @observers.extend(Enumerable)
29
+ @callbacks = Ref::WeakKeyMap.new
30
+ end
31
+
32
+ # Add an observer.
33
+ #
34
+ # @note You cannot add the same observer multiple times. The
35
+ # most recent call overrides any earlier calls; because
36
+ # of this you can change the method for an observer by
37
+ # calling add with a new method.
38
+ #
39
+ # @param [#hash] observer
40
+ # @param [String, Symbol] method
41
+ # @return observer, as passed in.
42
+ # @raise ArgumentError if observer does not respodn to method.
43
+ def add(observer, method = :update)
44
+ unless observer.respond_to?(method)
45
+ raise ArgumentError, "#{observer} does not respond to #{method}"
46
+ end
47
+
48
+ synchronize do
49
+ @callbacks[observer] = method
50
+ @observers[key(observer)] = observer
51
+ end
52
+ end
53
+
54
+ # Remove an observer.
55
+ #
56
+ # @param [#hash] observer
57
+ # @return [Object, nil] the observer, or nil if it was not an observer.
58
+ def delete(observer)
59
+ synchronize do
60
+ @callbacks.delete(observer)
61
+ @observers.delete(key(observer))
62
+ end
63
+ end
64
+
65
+ # Notify all observers, passing along all arguments and given block.
66
+ #
67
+ # @return [Array] all return values.
68
+ def notify(*args, &block)
69
+ synchronize do
70
+ @observers.map do |_, object|
71
+ callback = @callbacks[object]
72
+ # rare, but could happen if @callbacks is touched by finalizer
73
+ # before the @observers
74
+ next if callback.nil?
75
+ object.send(callback, *args, &block)
76
+ end
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ def key(observer)
83
+ observer.hash
84
+ end
85
+
86
+ def synchronize
87
+ @monitor.synchronize { return yield }
88
+ end
89
+ end
@@ -0,0 +1,98 @@
1
+ require 'set'
2
+
3
+ class WeakObservable
4
+ # The Hub is like a hash, mapping a key to an observable.
5
+ #
6
+ # Hub holds no strong references. Objects added as observers
7
+ # for a given key can be garbage collected. When all observers
8
+ # for a given key has been garbage collected, the observable
9
+ # for that key is also (eventually) garbage collected.
10
+ #
11
+ # This class is useful for having observers for a given key
12
+ # without necessarily protecting them from being collected by
13
+ # the garbage collector. Say, if you were mapping a pointer
14
+ # address to a bunch of ruby objects for FFI callbacks.
15
+ #
16
+ # @example
17
+ # $hub = WeakObservable::Hub.new
18
+ #
19
+ # def some_callback(address, some_arg)
20
+ # $hub[address].notify(some_arg)
21
+ # end
22
+ #
23
+ # object = Object.new
24
+ # $hub[pointer.address] = object
25
+ # SomeFFIBinding.register_callback(object.pointer, :some_callback)
26
+ class Hub
27
+ def initialize
28
+ @mapping = Ref::WeakValueMap.new
29
+ @monitor = Ref::SafeMonitor.new
30
+ end
31
+
32
+ # Add an object as an observer for updates related to the given key.
33
+ #
34
+ # @param [#hash, #eql?] key
35
+ # @param object
36
+ # @param [Symbol, String] method
37
+ # @return object
38
+ def add(key, object, method = :update)
39
+ synchronize do
40
+ observable = (@mapping[key] ||= WeakObservable.new)
41
+ backrefs_of(object) << observable
42
+ observable.add(object, method)
43
+ end
44
+ end
45
+
46
+ # Remove an object as an observer from updates related to the given key.
47
+ #
48
+ # @param [#hash, #eql?] key
49
+ # @param object
50
+ # @return [Object, nil] object, or nil if it was not an observer for that key
51
+ def delete(key, object)
52
+ synchronize do
53
+ if observable = @mapping[key]
54
+ backrefs_of(object).delete(observable)
55
+ observable.delete(object)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Notify all observers listening to events on the given key.
61
+ # All arguments are passed, as well as the given block.
62
+ #
63
+ # @param [#hash, #eql?] key
64
+ # @param args
65
+ # @return [Array] all return values
66
+ def notify(key, *args, &block)
67
+ synchronize do
68
+ if observable = @mapping[key]
69
+ observable.notify(*args, &block)
70
+ end
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def synchronize
77
+ @monitor.synchronize { return yield }
78
+ end
79
+
80
+ # The observers retain their observable, but the hub does not.
81
+ # This way we don’t need to garbage collect our mapping, since
82
+ # when all observers are gone, the observables for the address
83
+ # they are registered to is also eligible for garbage collection.
84
+ def backrefs_of(object)
85
+ # The object may be attached to several hubs, so make sure
86
+ # we don’t fuck up by mapping our own object_id first.
87
+ ivar = '@__observable__hubs__'
88
+ back_hash = object.instance_variable_get(ivar)
89
+ back_hash ||= Hash.new
90
+ object.instance_variable_set(ivar, back_hash)
91
+
92
+ # An object may also be attached to several addresses within
93
+ # this hub. We want to allow this, but we don’t care about
94
+ # the addresses so we just keep a list of observables.
95
+ back_hash[object_id] ||= Set.new
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ class WeakObservable
2
+ module Mixin
3
+ def observers
4
+ @__weak_observable_observers__ ||= WeakObservable.new
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ class WeakObservable
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1 @@
1
+ require 'weak_observable'
@@ -0,0 +1,177 @@
1
+ describe WeakObservable::Hub do
2
+ let(:observable) { WeakObservable::Hub.new }
3
+ let(:observer) { stub(:update => 1) }
4
+ let(:key) { 1337 }
5
+ let(:other_key) { 0xDEADBEEF }
6
+
7
+ describe "#add and #notify" do
8
+ it "adds an observer for given key" do
9
+ observer.should_receive(:update)
10
+
11
+ observable.add(key, observer)
12
+ observable.notify(key)
13
+ end
14
+
15
+ it "allows us to specify callback method" do
16
+ observer.should_receive(:trigger)
17
+
18
+ observable.add(key, observer, :trigger)
19
+ observable.notify(key)
20
+ end
21
+
22
+ it "allows the same observer for multiple keys" do
23
+ observer.should_receive(:update).twice
24
+
25
+ observable.add(key, observer)
26
+ observable.add(other_key, observer)
27
+
28
+ observable.notify(key)
29
+ observable.notify(other_key)
30
+ end
31
+
32
+ it "allows different methods for different keys" do
33
+ observer.should_receive(:update).once
34
+ observer.should_receive(:trigger).once
35
+
36
+ observable.add(key, observer)
37
+ observable.add(other_key, observer, :trigger)
38
+
39
+ observable.notify(key)
40
+ observable.notify(other_key)
41
+ end
42
+
43
+ it "does not allow duplicates for the same key" do
44
+ observer.should_receive(:update).once
45
+
46
+ observable.add(key, observer)
47
+ observable.notify(key)
48
+ end
49
+
50
+ it "raises an error if observer does not respond to method" do
51
+ expect { observable.add(key, stub) }.to raise_error(ArgumentError)
52
+ end
53
+ end
54
+
55
+ describe "#notify" do
56
+ it "notifies observers only with the same key" do
57
+ observer.should_not_receive(:update)
58
+
59
+ observable.add(key, observer)
60
+ observable.notify(other_key)
61
+ end
62
+
63
+ it "passes along any args and given block" do
64
+ observer.should_receive(:update).with(1, 2).and_return do |x, y, &block|
65
+ block.call(x, y)
66
+ end
67
+
68
+ observable.add(key, observer)
69
+ observable.notify(key, 1, 2) { |x, y| x + y }.should eq [3]
70
+ end
71
+
72
+ it "returns all return values" do
73
+ observer1 = stub(:update => 1)
74
+ observer2 = stub(:update => 2)
75
+
76
+ observable.add(key, observer1)
77
+ observable.add(key, observer2)
78
+
79
+ observable.notify(key).should eq [1, 2]
80
+ end
81
+ end
82
+
83
+ describe "#delete" do
84
+ it "deletes the observer with the given key" do
85
+ observer.should_not_receive(:update)
86
+
87
+ observable.add(key, observer)
88
+ observable.delete(key, observer)
89
+ observable.notify(key).should eq []
90
+ end
91
+
92
+ it "does not crash trying to remove a non-existing observer" do
93
+ observable.add(key, observer)
94
+ expect { observable.delete(key, stub) }.to_not raise_error
95
+ end
96
+
97
+ it "does not crash trying to remove an observer with a non-existing key" do
98
+ expect { observable.delete(key, observer) }.to_not raise_error
99
+ end
100
+
101
+ it "keeps the observer for other keys" do
102
+ observer1 = stub(:update => 1)
103
+ observer2 = stub(:update => 2)
104
+
105
+ observer2.should_not_receive(:update)
106
+
107
+ observable.add(key, observer1)
108
+ observable.add(other_key, observer2)
109
+ observable.delete(other_key, observer2)
110
+
111
+ observable.notify(key).should eq [1]
112
+ observable.notify(other_key).should eq []
113
+ end
114
+
115
+ it "does not remove any other observer for a given key" do
116
+ observer1 = stub(:update => 1)
117
+ observer2 = stub(:update => 2)
118
+
119
+ observer2.should_not_receive(:update)
120
+
121
+ observable.add(key, observer1)
122
+ observable.add(key, observer2)
123
+ observable.delete(key, observer2)
124
+
125
+ observable.notify(key).should eq [1]
126
+ end
127
+ end
128
+
129
+ specify "observers can be garbage collected" do
130
+ counter = 0
131
+ finalizer = lambda { |_| counter += 1 }
132
+
133
+ threshold = 5
134
+ stretches = 5
135
+
136
+ threshold.times do
137
+ # stub with return value for method cannot be garbage collected.
138
+ observer = OpenStruct.new(:update => nil)
139
+ observable.add(key, observer)
140
+ ObjectSpace.define_finalizer(observer, finalizer)
141
+ end
142
+
143
+ (threshold * stretches).times do
144
+ GC.start
145
+ sleep 0.001
146
+ end
147
+
148
+ # if one is garbage collected, we can expect all to eventually be
149
+ # as well
150
+ counter.should be > 1
151
+ end
152
+
153
+ specify "entire keys can be garbage collected" do
154
+ counter = 0
155
+ finalizer = lambda { |_| counter += 1 }
156
+
157
+ threshold = 5
158
+ stretches = 5
159
+
160
+ threshold.times do |i|
161
+ # stub with return value for method cannot be garbage collected.
162
+ observer = OpenStruct.new(:update => nil)
163
+ observable.add(key + i, observer)
164
+ ObjectSpace.define_finalizer(observer, finalizer)
165
+ end
166
+
167
+ (threshold * stretches).times do
168
+ GC.start
169
+ sleep 0.001
170
+ end
171
+
172
+ # some very intimate introspection here
173
+ observable.instance_eval do
174
+ @mapping.to_a.length.should < threshold
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,14 @@
1
+ describe WeakObservable::Mixin do
2
+ let(:klass) { Class.new { include WeakObservable::Mixin } }
3
+ let(:observable) { klass.new }
4
+
5
+ describe "#observers" do
6
+ it "is a weak observable" do
7
+ observable.observers.should be_a WeakObservable
8
+ end
9
+
10
+ it "is memoized" do
11
+ observable.observers.should eql observable.observers
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,110 @@
1
+ require 'ostruct'
2
+
3
+ describe WeakObservable do
4
+ let(:observable) { WeakObservable.new }
5
+ let(:observer) { stub(:update => nil) }
6
+
7
+ it "has a version" do
8
+ WeakObservable::VERSION.should be_a String
9
+ end
10
+
11
+ describe "#add and #notify" do
12
+ it "adds an observer" do
13
+ observer.should_receive(:update)
14
+
15
+ observable.add(observer)
16
+ observable.notify
17
+ end
18
+
19
+ it "allows us to specify callback method" do
20
+ observer.should_receive(:trigger)
21
+
22
+ observable.add(observer, :trigger)
23
+ observable.notify
24
+ end
25
+
26
+ it "does not allow duplicates" do
27
+ observer.should_receive(:update).once
28
+
29
+ observable.add(observer)
30
+ observable.add(observer)
31
+ observable.notify
32
+ end
33
+
34
+ it "raises an error if observer does not respond to method" do
35
+ expect { observable.add(stub) }.to raise_error(ArgumentError)
36
+ end
37
+ end
38
+
39
+ describe "#notify" do
40
+ it "passes along any args given block" do
41
+ observer.should_receive(:update).with(1, 2).and_return do |&block|
42
+ block.call(2)
43
+ end
44
+
45
+ observable.add(observer)
46
+ observable.notify(1, 2) { |x| x * 2 }.should eq [4]
47
+ end
48
+
49
+ it "returns all return values" do
50
+ observer1 = stub(:update => 1)
51
+ observer2 = stub(:update => 2)
52
+
53
+ observable.add(observer1)
54
+ observable.add(observer2)
55
+
56
+ observable.notify.should eq [1, 2]
57
+ end
58
+ end
59
+
60
+ describe "#delete" do
61
+ it "removes the observer" do
62
+ observer.should_not_receive(:update)
63
+
64
+ observable.add(observer)
65
+ observable.delete(observer)
66
+ observable.notify.should eq []
67
+ end
68
+
69
+ it "does not remove any other observer" do
70
+ observer1 = stub(:update => 1)
71
+ observer2 = stub(:update => 2)
72
+
73
+ observer2.should_not_receive(:update)
74
+
75
+ observable.add(observer1)
76
+ observable.add(observer2)
77
+ observable.delete(observer2)
78
+
79
+ observable.notify.should eq [1]
80
+ end
81
+
82
+ it "does not crash removing a non-existing observer" do
83
+ expect { observable.delete(observer) }.to_not raise_error
84
+ end
85
+ end
86
+
87
+ specify "observers can be garbage collected" do
88
+ counter = 0
89
+ finalizer = lambda { |_| counter += 1 }
90
+
91
+ threshold = 5
92
+ stretches = 5
93
+
94
+ threshold.times do
95
+ # stub with return value for method cannot be garbage collected.
96
+ observer = OpenStruct.new(:update => nil)
97
+ observable.add(observer)
98
+ ObjectSpace.define_finalizer(observer, finalizer)
99
+ end
100
+
101
+ (threshold * stretches).times do
102
+ GC.start
103
+ sleep 0.001
104
+ end
105
+
106
+ # if one is garbage collected, we can expect all to eventually be
107
+ # as well
108
+ counter.should be > 1
109
+ end
110
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'weak_observable/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "weak_observable"
8
+ gem.version = WeakObservable::VERSION
9
+ gem.authors = ["Kim Burgestrand"]
10
+ gem.email = ["kim@burgestrand.se"]
11
+ gem.summary = "Like Observer from the standard library, but allows the subscribers to be garbage collected."
12
+ gem.description = <<-DESC.gsub(/ {2,}/, '')
13
+ Observable::Weak is very similar to Observable from ruby’ standard library, but
14
+ with the very important difference in that it allows it’s subscribers to be
15
+ garbage collected.
16
+ DESC
17
+ gem.homepage = "http://github.com/Burgestrand/weak_observable"
18
+
19
+ gem.files = `git ls-files`.split($/)
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ["lib"]
22
+
23
+ gem.add_dependency 'ref', '~> 1.0.0'
24
+ gem.add_development_dependency 'rspec', '~> 2.11.0'
25
+ end
metadata ADDED
@@ -0,0 +1,106 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: weak_observable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kim Burgestrand
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ref
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 2.11.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 2.11.0
46
+ description: ! 'Observable::Weak is very similar to Observable from ruby’ standard
47
+ library, but
48
+
49
+ with the very important difference in that it allows it’s subscribers to be
50
+
51
+ garbage collected.
52
+
53
+ '
54
+ email:
55
+ - kim@burgestrand.se
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - .gitignore
61
+ - .rspec
62
+ - CHANGELOG
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - lib/weak_observable.rb
68
+ - lib/weak_observable/hub.rb
69
+ - lib/weak_observable/mixin.rb
70
+ - lib/weak_observable/version.rb
71
+ - spec/spec_helper.rb
72
+ - spec/weak_observable/hub_spec.rb
73
+ - spec/weak_observable/mixin_spec.rb
74
+ - spec/weak_observable/weak_observable_spec.rb
75
+ - weak_observable.gemspec
76
+ homepage: http://github.com/Burgestrand/weak_observable
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.24
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Like Observer from the standard library, but allows the subscribers to be
100
+ garbage collected.
101
+ test_files:
102
+ - spec/spec_helper.rb
103
+ - spec/weak_observable/hub_spec.rb
104
+ - spec/weak_observable/mixin_spec.rb
105
+ - spec/weak_observable/weak_observable_spec.rb
106
+ has_rdoc: