├── script ├── bootstrap ├── watch └── test ├── Rakefile ├── lib ├── ag │ ├── feed.rb │ ├── version.rb │ ├── connection.rb │ ├── event.rb │ ├── object.rb │ ├── client.rb │ ├── adapters │ │ ├── memory.rb │ │ ├── redis_push.rb │ │ ├── active_record_pull.rb │ │ ├── sequel_pull_compact.rb │ │ ├── sequel_pull.rb │ │ └── sequel_push.rb │ └── spec │ │ └── adapter.rb └── ag.rb ├── test ├── helper.rb ├── adapters │ ├── memory_test.rb │ ├── redis_push_test.rb │ ├── sequel_pull_compact_test.rb │ ├── sequel_pull_test.rb │ ├── sequel_push_test.rb │ └── active_record_pull_test.rb └── client_test.rb ├── .gitignore ├── Gemfile ├── examples ├── setup.rb └── readme.rb ├── ag.gemspec ├── LICENSE.txt ├── README.md └── Guardfile /script/bootstrap: -------------------------------------------------------------------------------- 1 | bundle --quiet 2 | -------------------------------------------------------------------------------- /script/watch: -------------------------------------------------------------------------------- 1 | bundle exec guard 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /lib/ag/feed.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | class Feed 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/ag/version.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | require "ag" 3 | 4 | class Ag::Test < Minitest::Test 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /lib/ag.rb: -------------------------------------------------------------------------------- 1 | require "ag/version" 2 | 3 | module Ag 4 | end 5 | 6 | require "ag/object" 7 | require "ag/connection" 8 | require "ag/event" 9 | require "ag/feed" 10 | require "ag/client" 11 | require "ag/adapters/memory" 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "activerecord", "~> 5.2.1" 5 | gem "redis", "~> 3.2.1" 6 | gem "sequel", "~> 4.19.0" 7 | gem "sqlite3", "~> 1.3.10" 8 | 9 | gem "guard", "~> 2.13.0" 10 | gem "guard-minitest", "~> 2.4.4" 11 | -------------------------------------------------------------------------------- /examples/setup.rb: -------------------------------------------------------------------------------- 1 | # Nothing to see here... move along. 2 | # Sets up load path for examples and requires some stuff 3 | require "pp" 4 | require "pathname" 5 | 6 | root_path = Pathname(__FILE__).dirname.join("..").expand_path 7 | lib_path = root_path.join("lib") 8 | $:.unshift(lib_path) 9 | -------------------------------------------------------------------------------- /test/adapters/memory_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/memory" 3 | require "ag/spec/adapter" 4 | require "securerandom" 5 | 6 | class AdaptersMemoryTest < Ag::Test 7 | def setup 8 | @source = {} 9 | end 10 | 11 | def adapter 12 | @adapter ||= Ag::Adapters::Memory.new(@source) 13 | end 14 | 15 | include Ag::Spec::Adapter 16 | end 17 | -------------------------------------------------------------------------------- /test/adapters/redis_push_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/redis_push" 3 | require "ag/spec/adapter" 4 | 5 | class AdaptersRedisPushTest < Ag::Test 6 | def setup 7 | @redis = Redis.new(:port => ENV.fetch("GH_REDIS_PORT", 6379).to_i) 8 | @redis.flushdb 9 | end 10 | 11 | def adapter 12 | @adapter ||= Ag::Adapters::RedisPush.new(@redis) 13 | end 14 | 15 | include Ag::Spec::Adapter 16 | end 17 | -------------------------------------------------------------------------------- /lib/ag/connection.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Ag 4 | class Connection 5 | attr_reader :id 6 | attr_reader :producer 7 | attr_reader :consumer 8 | attr_reader :created_at 9 | 10 | def initialize(attributes = {}) 11 | @id = attributes[:id] 12 | @producer = attributes[:producer] 13 | @consumer = attributes[:consumer] 14 | @created_at = attributes[:created_at] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ag/event.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | class Event 3 | attr_reader :id 4 | attr_reader :producer 5 | attr_reader :object 6 | attr_reader :verb 7 | attr_reader :created_at 8 | 9 | def initialize(attrs = {}) 10 | @id = attrs[:id] 11 | @producer = attrs.fetch(:producer) 12 | @object = attrs.fetch(:object) 13 | @verb = attrs.fetch(:verb) 14 | @created_at = attrs.fetch(:created_at) { Time.now.utc } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ag/object.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | class Object 3 | Separator = ";".freeze 4 | 5 | def self.from_key(key, separator = Separator) 6 | new(*key.split(Separator)) 7 | end 8 | 9 | attr_reader :type 10 | attr_reader :id 11 | 12 | def initialize(type, id) 13 | @type = type 14 | @id = id 15 | end 16 | 17 | def key(*suffixes) 18 | [@type, @id].concat(Array(suffixes)).join(Separator) 19 | end 20 | 21 | def ==(other) 22 | self.class == other.class && 23 | self.type == other.type && 24 | self.id == other.id 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #/ Usage: test [individual test file] 3 | #/ 4 | #/ Bootstrap and run all tests or an individual test. 5 | #/ 6 | #/ Examples: 7 | #/ 8 | #/ # run all tests 9 | #/ test 10 | #/ 11 | #/ # run individual test 12 | #/ test test/controller_instrumentation_test.rb 13 | #/ 14 | 15 | set -e 16 | cd $(dirname "$0")/.. 17 | 18 | [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && { 19 | grep '^#/' <"$0"| cut -c4- 20 | exit 0 21 | } 22 | 23 | ruby -I lib -I test -r rubygems \ 24 | -e 'require "bundler/setup"' \ 25 | -e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@" 26 | -------------------------------------------------------------------------------- /lib/ag/client.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | class Client 3 | def initialize(adapter) 4 | @adapter = adapter 5 | end 6 | 7 | def connect(consumer, producer) 8 | @adapter.connect(consumer, producer) 9 | end 10 | 11 | def produce(event) 12 | @adapter.produce(event) 13 | end 14 | 15 | def connected?(consumer, producer) 16 | @adapter.connected?(consumer, producer) 17 | end 18 | 19 | def consumers(producer, options = {}) 20 | @adapter.consumers(producer, options) 21 | end 22 | 23 | def producers(consumer, options = {}) 24 | @adapter.producers(consumer, options) 25 | end 26 | 27 | def timeline(consumer, options = {}) 28 | @adapter.timeline(consumer, options) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/adapters/sequel_pull_compact_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/sequel_pull_compact" 3 | require "ag/spec/adapter" 4 | 5 | class AdaptersSequelPullCompactTest < Ag::Test 6 | def setup 7 | Sequel.default_timezone = :utc 8 | @db = Sequel.sqlite 9 | 10 | @db.create_table :connections do 11 | primary_key :id 12 | String :consumer_id 13 | String :producer_id 14 | Time :created_at 15 | index [:consumer_id, :producer_id], unique: true 16 | end 17 | 18 | @db.create_table :events do 19 | primary_key :id 20 | String :producer_id 21 | String :object_id 22 | String :verb 23 | Time :created_at 24 | end 25 | end 26 | 27 | def adapter 28 | @adapter ||= Ag::Adapters::SequelPullCompact.new(@db) 29 | end 30 | 31 | include Ag::Spec::Adapter 32 | end 33 | -------------------------------------------------------------------------------- /examples/readme.rb: -------------------------------------------------------------------------------- 1 | require_relative "setup" 2 | require "ag" 3 | 4 | adapter = Ag::Adapters::Memory.new 5 | client = Ag::Client.new(adapter) 6 | john = Ag::Object.new("User", "1") 7 | steve = Ag::Object.new("User", "2") 8 | presentation = Ag::Object.new("Presentation", "1") 9 | event = Ag::Event.new({ 10 | producer: steve, 11 | object: presentation, 12 | verb: "upload_presentation", 13 | }) 14 | 15 | # connect john to steve 16 | pp connect: client.connect(john, steve) 17 | 18 | # is john connected to steve 19 | pp connected?: client.connected?(john, steve) 20 | 21 | # consumers of steve 22 | pp consumers: client.consumers(steve) 23 | 24 | # producers john is connected to 25 | pp producers: client.producers(john) 26 | 27 | # produce an event for steve 28 | pp produce: client.produce(event) 29 | 30 | # get the timeline of events for john based on the producers john follows 31 | pp timeline: client.timeline(john) 32 | -------------------------------------------------------------------------------- /test/adapters/sequel_pull_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/sequel_pull" 3 | require "ag/spec/adapter" 4 | 5 | class AdaptersSequelPullTest < Ag::Test 6 | def setup 7 | Sequel.default_timezone = :utc 8 | @db = Sequel.sqlite 9 | 10 | @db.create_table :connections do 11 | primary_key :id 12 | String :consumer_id 13 | String :consumer_type 14 | String :producer_id 15 | String :producer_type 16 | Time :created_at 17 | index [:consumer_id, :consumer_type, :producer_id, :producer_type], unique: true 18 | end 19 | 20 | @db.create_table :events do 21 | primary_key :id 22 | String :producer_id 23 | String :producer_type 24 | String :object_id 25 | String :object_type 26 | String :verb 27 | Time :created_at 28 | end 29 | end 30 | 31 | def adapter 32 | @adapter ||= Ag::Adapters::SequelPull.new(@db) 33 | end 34 | 35 | include Ag::Spec::Adapter 36 | end 37 | -------------------------------------------------------------------------------- /ag.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "ag/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "ag" 8 | spec.version = Ag::VERSION 9 | spec.authors = ["John Nunemaker"] 10 | spec.email = ["nunemaker@gmail.com"] 11 | spec.summary = %q{Producers, consumers, connections and timelines.} 12 | spec.description = %q{Producers, consumers, connections and timelines.} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.6" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | spec.add_development_dependency "minitest", "~> 5.5.1" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 John Nunemaker 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. 23 | -------------------------------------------------------------------------------- /test/adapters/sequel_push_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/sequel_push" 3 | require "ag/spec/adapter" 4 | 5 | class AdaptersSequelPushTest < Ag::Test 6 | def setup 7 | Sequel.default_timezone = :utc 8 | @db = Sequel.sqlite 9 | 10 | @db.create_table :connections do 11 | primary_key :id 12 | String :consumer_id 13 | String :consumer_type 14 | String :producer_id 15 | String :producer_type 16 | Time :created_at 17 | index [:consumer_id, :consumer_type, :producer_id, :producer_type], unique: true 18 | end 19 | 20 | @db.create_table :events do 21 | primary_key :id 22 | String :producer_id 23 | String :producer_type 24 | String :object_id 25 | String :object_type 26 | String :verb 27 | Time :created_at 28 | end 29 | 30 | @db.create_table :timelines do 31 | primary_key :id 32 | String :consumer_id 33 | String :consumer_type 34 | String :event_id 35 | Time :created_at 36 | end 37 | end 38 | 39 | def adapter 40 | @adapter ||= Ag::Adapters::SequelPush.new(@db) 41 | end 42 | 43 | include Ag::Spec::Adapter 44 | end 45 | -------------------------------------------------------------------------------- /test/adapters/active_record_pull_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helper" 2 | require "ag/adapters/active_record_pull" 3 | require "ag/spec/adapter" 4 | 5 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", 6 | database: ":memory:") 7 | 8 | ActiveRecord::Base.connection.create_table :ag_connections do |t| 9 | t.string :consumer_id, null: false 10 | t.string :consumer_type, null: false 11 | t.string :producer_id, null: false 12 | t.string :producer_type, null: false 13 | t.timestamps 14 | t.index [:consumer_id, :consumer_type, :producer_id, :producer_type], unique: true, name: :consumer_to_producer 15 | end 16 | 17 | ActiveRecord::Base.connection.create_table :ag_events do |t| 18 | t.string :producer_id, null: false 19 | t.string :producer_type, null: false 20 | t.string :object_id, null: false 21 | t.string :object_type, null: false 22 | t.string :verb, null: false 23 | t.datetime :created_at, null: false 24 | end 25 | 26 | class AdaptersActiveRecordPullTest < Ag::Test 27 | def setup 28 | Ag::Adapters::ActiveRecordPull::Connection.delete_all 29 | Ag::Adapters::ActiveRecordPull::Event.delete_all 30 | end 31 | 32 | def adapter 33 | @adapter ||= Ag::Adapters::ActiveRecordPull.new 34 | end 35 | 36 | include Ag::Spec::Adapter 37 | end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ag 2 | 3 | WORK IN PROGRESS... 4 | 5 | Experiments in describing feeds/timelines of events in code based on adapters so things can work at most levels of throughput. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem "ag" 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install ag 22 | 23 | ## Usage 24 | 25 | ```ruby 26 | require "ag" 27 | 28 | adapter = Ag::Adapters::Memory.new 29 | client = Ag::Client.new(adapter) 30 | john = Ag::Object.new("User", "1") 31 | steve = Ag::Object.new("User", "2") 32 | presentation = Ag::Object.new("Presentation", "1") 33 | event = Ag::Event.new({ 34 | producer: steve, 35 | object: presentation, 36 | verb: "upload_presentation", 37 | }) 38 | 39 | # connect john to steve 40 | pp connect: client.connect(john, steve) 41 | 42 | # is john connected to steve 43 | pp connected?: client.connected?(john, steve) 44 | 45 | # consumers of steve 46 | pp consumers: client.consumers(steve) 47 | 48 | # producers john is connected to 49 | pp producers: client.producers(john) 50 | 51 | # produce an event for steve 52 | pp produce: client.produce(event) 53 | 54 | # get the timeline of events for john based on the producers john follows 55 | pp timeline: client.timeline(john) 56 | ``` 57 | 58 | ## Contributing 59 | 60 | 1. Fork it ( https://github.com/jnunemaker/ag/fork ) 61 | 2. Create your feature branch (`git checkout -b my-new-feature`) 62 | 3. Commit your changes (`git commit -am 'Add some feature'`) 63 | 4. Push to the branch (`git push origin my-new-feature`) 64 | 5. Create a new Pull Request 65 | -------------------------------------------------------------------------------- /lib/ag/adapters/memory.rb: -------------------------------------------------------------------------------- 1 | require "securerandom" 2 | 3 | module Ag 4 | module Adapters 5 | class Memory 6 | def initialize(source = {}) 7 | @source = source 8 | @source[:connections] ||= [] 9 | @source[:events] ||= [] 10 | end 11 | 12 | def connect(consumer, producer) 13 | @source[:connections] << Connection.new({ 14 | id: SecureRandom.uuid, 15 | created_at: Time.now.utc, 16 | consumer: consumer, 17 | producer: producer, 18 | }) 19 | end 20 | 21 | def produce(event) 22 | result = Ag::Event.new({ 23 | id: SecureRandom.uuid, 24 | producer: event.producer, 25 | object: event.object, 26 | verb: event.verb, 27 | }) 28 | @source[:events] << result 29 | result 30 | end 31 | 32 | def connected?(consumer, producer) 33 | !@source[:connections].detect { |connection| 34 | connection.consumer == consumer && 35 | connection.producer == producer 36 | }.nil? 37 | end 38 | 39 | def consumers(producer, options = {}) 40 | @source[:connections].select { |connection| 41 | connection.producer == producer 42 | }.reverse[options.fetch(:offset, 0), options.fetch(:limit, 30)] 43 | end 44 | 45 | def producers(consumer, options = {}) 46 | @source[:connections].select { |connection| 47 | connection.consumer == consumer 48 | }.reverse[options.fetch(:offset, 0), options.fetch(:limit, 30)] 49 | end 50 | 51 | def timeline(consumer, options = {}) 52 | producers = producers(consumer).map(&:producer) 53 | 54 | Array(@source[:events]).select { |event| 55 | producers.include?(event.producer) 56 | }.sort_by { |event| 57 | -event.created_at.to_f 58 | }[options.fetch(:offset, 0), options.fetch(:limit, 30)] 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | guard :minitest do 19 | # with Minitest::Unit 20 | watch(%r{^test/(.*)\/?(.*)_test\.rb$}) 21 | watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" } 22 | watch(%r{^test/helper\.rb$}) { 'test' } 23 | 24 | # with Minitest::Spec 25 | # watch(%r{^spec/(.*)_spec\.rb$}) 26 | # watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 27 | # watch(%r{^spec/spec_helper\.rb$}) { 'spec' } 28 | 29 | # Rails 4 30 | # watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" } 31 | # watch(%r{^app/controllers/application_controller\.rb$}) { 'test/controllers' } 32 | # watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" } 33 | # watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" } 34 | # watch(%r{^lib/(.+)\.rb$}) { |m| "test/lib/#{m[1]}_test.rb" } 35 | # watch(%r{^test/.+_test\.rb$}) 36 | # watch(%r{^test/test_helper\.rb$}) { 'test' } 37 | 38 | # Rails < 4 39 | # watch(%r{^app/controllers/(.*)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" } 40 | # watch(%r{^app/helpers/(.*)\.rb$}) { |m| "test/helpers/#{m[1]}_test.rb" } 41 | # watch(%r{^app/models/(.*)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" } 42 | end 43 | -------------------------------------------------------------------------------- /test/client_test.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class ClientTest < Ag::Test 4 | def test_initializes_with_adapter 5 | adapter = Ag::Adapters::Memory.new 6 | client = Ag::Client.new(adapter) 7 | assert_instance_of Ag::Client, client 8 | end 9 | 10 | def test_forwards_connect_to_adapter 11 | args = [consumer, producer] 12 | result = true 13 | mock_adapter = Minitest::Mock.new 14 | mock_adapter.expect(:connect, result, args) 15 | 16 | client = Ag::Client.new(mock_adapter) 17 | assert_equal result, client.connect(*args) 18 | 19 | mock_adapter.verify 20 | end 21 | 22 | def test_forwards_produce_to_adapter 23 | args = [event] 24 | result = [producer] 25 | mock_adapter = Minitest::Mock.new 26 | mock_adapter.expect(:produce, result, args) 27 | 28 | client = Ag::Client.new(mock_adapter) 29 | assert_equal result, client.produce(*args) 30 | 31 | mock_adapter.verify 32 | end 33 | 34 | def test_forwards_connected_to_adapter 35 | args = [consumer, producer] 36 | result = true 37 | mock_adapter = Minitest::Mock.new 38 | mock_adapter.expect(:connected?, result, args) 39 | 40 | client = Ag::Client.new(mock_adapter) 41 | assert_equal result, client.connected?(*args) 42 | 43 | mock_adapter.verify 44 | end 45 | 46 | def test_forwards_consumers_to_adapter 47 | args = [producer, {}] 48 | result = [consumer] 49 | mock_adapter = Minitest::Mock.new 50 | mock_adapter.expect(:consumers, result, args) 51 | 52 | client = Ag::Client.new(mock_adapter) 53 | assert_equal result, client.consumers(*args) 54 | 55 | mock_adapter.verify 56 | end 57 | 58 | def test_forwards_producers_to_adapter 59 | args = [consumer, {}] 60 | result = [producer] 61 | mock_adapter = Minitest::Mock.new 62 | mock_adapter.expect(:producers, result, args) 63 | 64 | client = Ag::Client.new(mock_adapter) 65 | assert_equal result, client.producers(*args) 66 | 67 | mock_adapter.verify 68 | end 69 | 70 | def test_forwards_timeline_to_adapter 71 | args = [consumer, {}] 72 | result = [event] 73 | mock_adapter = Minitest::Mock.new 74 | mock_adapter.expect(:timeline, result, args) 75 | 76 | client = Ag::Client.new(mock_adapter) 77 | assert_equal result, client.timeline(*args) 78 | 79 | mock_adapter.verify 80 | end 81 | 82 | private 83 | 84 | def event 85 | @event ||= Ag::Event.new({ 86 | producer: producer, 87 | object: object, 88 | verb: verb, 89 | }) 90 | end 91 | 92 | def verb 93 | "follow" 94 | end 95 | 96 | def consumer 97 | @consumer ||= Ag::Object.new("User", "1") 98 | end 99 | 100 | def producer 101 | @producer ||= Ag::Object.new("User", "1") 102 | end 103 | 104 | def object 105 | @object ||= Ag::Object.new("User", "3") 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/ag/adapters/redis_push.rb: -------------------------------------------------------------------------------- 1 | require "redis" 2 | require "json" 3 | require "securerandom" 4 | 5 | module Ag 6 | module Adapters 7 | class RedisPush 8 | def initialize(redis) 9 | @redis = redis 10 | end 11 | 12 | def connect(consumer, producer) 13 | @redis.pipelined do |redis| 14 | redis.zadd(producer.key("consumers"), Time.now.to_f, consumer.key) 15 | redis.zadd(consumer.key("producers"), Time.now.to_f, producer.key) 16 | end 17 | end 18 | 19 | def produce(event) 20 | value = { 21 | id: SecureRandom.uuid, 22 | producer_id: event.producer.id, 23 | producer_type: event.producer.type, 24 | object_id: event.object.id, 25 | object_type: event.object.type, 26 | verb: event.verb, 27 | } 28 | json_value = JSON.dump(value) 29 | created_at_float = event.created_at.to_f 30 | 31 | # FIXME: This is terrible for large number of consumers. Would be better 32 | # to do in consumer batches. 33 | consumers = consumers(event.producer) 34 | @redis.pipelined do |redis| 35 | redis.set("events:#{value[:id]}", json_value) 36 | redis.zadd("events", created_at_float, value[:id]) 37 | consumers.each do |connection| 38 | redis.zadd(connection.consumer.key("timeline"), created_at_float, value[:id]) 39 | end 40 | end 41 | 42 | Ag::Event.new({ 43 | id: value[:id], 44 | producer: event.producer, 45 | object: event.object, 46 | verb: event.verb, 47 | created_at: event.created_at, 48 | }) 49 | end 50 | 51 | def connected?(consumer, producer) 52 | !@redis.zscore(consumer.key("producers"), producer.key).nil? 53 | end 54 | 55 | def consumers(producer, options = {}) 56 | limit = options.fetch(:limit, 30) 57 | offset = options.fetch(:offset, 0) 58 | start = offset 59 | finish = start + limit - 1 60 | 61 | @redis.zrevrange(producer.key("consumers"), start, finish, with_scores: true).map { |key, value| 62 | Ag::Connection.new({ 63 | consumer: Ag::Object.from_key(key), 64 | producer: producer, 65 | created_at: Time.at(value).utc 66 | }) 67 | } 68 | end 69 | 70 | def producers(consumer, options = {}) 71 | limit = options.fetch(:limit, 30) 72 | offset = options.fetch(:offset, 0) 73 | start = offset 74 | finish = start + limit - 1 75 | 76 | @redis.zrevrange(consumer.key("producers"), start, finish, with_scores: true).map { |key, value| 77 | Ag::Connection.new({ 78 | producer: Ag::Object.from_key(key), 79 | consumer: consumer, 80 | created_at: Time.at(value).utc 81 | }) 82 | } 83 | end 84 | 85 | def timeline(consumer, options = {}) 86 | limit = options.fetch(:limit, 30) 87 | offset = options.fetch(:offset, 0) 88 | start = offset 89 | finish = start + limit - 1 90 | 91 | # get all the event ids 92 | rows = @redis.zrevrange(consumer.key("timeline"), start, finish, with_scores: true) 93 | 94 | # mget all events 95 | # FIXME: this is most likely terrible for really large number 96 | # of events being fetched; should probably mget in batches 97 | event_keys = rows.map { |row| "events:#{row[0]}" } 98 | redis_events = @redis.mget(event_keys).inject({}) { |hash, json| 99 | event = JSON.load(json) 100 | hash[event["id"]] = event 101 | hash 102 | } 103 | 104 | # build the event instances 105 | rows.map { |row| 106 | event_id, score = row 107 | hash = redis_events.fetch(event_id) 108 | created_at = Time.at(score).utc 109 | producer = Ag::Object.new(hash["producer_type"], hash.fetch("producer_id")) 110 | object = Ag::Object.new(hash["object_type"], hash.fetch("object_id")) 111 | 112 | Ag::Event.new({ 113 | id: event_id, 114 | producer: producer, 115 | object: object, 116 | verb: hash["verb"], 117 | created_at: created_at, 118 | }) 119 | } 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/ag/adapters/active_record_pull.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | module Ag 4 | module Adapters 5 | class ActiveRecordPull 6 | # Private: Do not use outside of this adapter. 7 | class Connection < ::ActiveRecord::Base 8 | self.table_name = [ 9 | ::ActiveRecord::Base.table_name_prefix, 10 | "ag_connections", 11 | ::ActiveRecord::Base.table_name_suffix, 12 | ].join 13 | end 14 | 15 | # Private: Do not use outside of this adapter. 16 | class Event < ::ActiveRecord::Base 17 | self.table_name = [ 18 | ::ActiveRecord::Base.table_name_prefix, 19 | "ag_events", 20 | ::ActiveRecord::Base.table_name_suffix, 21 | ].join 22 | end 23 | 24 | def connect(consumer, producer) 25 | created_at = Time.now.utc 26 | connection = Connection.create({ 27 | consumer_id: consumer.id, 28 | consumer_type: consumer.type, 29 | producer_id: producer.id, 30 | producer_type: producer.type, 31 | created_at: created_at, 32 | }) 33 | 34 | Ag::Connection.new({ 35 | id: connection.id, 36 | created_at: created_at, 37 | consumer: consumer, 38 | producer: producer, 39 | }) 40 | end 41 | 42 | def produce(event) 43 | created_at = Time.now.utc 44 | ar_event = Event.create({ 45 | producer_type: event.producer.type, 46 | producer_id: event.producer.id, 47 | object_type: event.object.type, 48 | object_id: event.object.id, 49 | verb: event.verb, 50 | created_at: created_at, 51 | }) 52 | 53 | Ag::Event.new({ 54 | id: ar_event.id, 55 | created_at: created_at, 56 | producer: event.producer, 57 | object: event.object, 58 | verb: event.verb, 59 | }) 60 | end 61 | 62 | def connected?(consumer, producer) 63 | Connection. 64 | where(consumer_id: consumer.id, consumer_type: consumer.type). 65 | where(producer_id: producer.id, producer_type: producer.type). 66 | exists? 67 | end 68 | 69 | def consumers(producer, options = {}) 70 | connections = Connection. 71 | select(:id, :created_at, :consumer_id, :consumer_type, :producer_id, :producer_type). 72 | where(producer_id: producer.id, producer_type: producer.type). 73 | order("created_at DESC"). 74 | limit(options.fetch(:limit, 30)). 75 | offset(options.fetch(:offset, 0)) 76 | 77 | connections.map do |connection| 78 | Ag::Connection.new({ 79 | id: connection.id, 80 | created_at: connection.created_at, 81 | consumer: Ag::Object.new(connection.consumer_type, connection.consumer_id), 82 | producer: Ag::Object.new(connection.producer_type, connection.producer_id), 83 | }) 84 | end 85 | end 86 | 87 | def producers(consumer, options = {}) 88 | connections = Connection. 89 | select(:id, :created_at, :consumer_id, :consumer_type, :producer_id, :producer_type). 90 | where(consumer_id: consumer.id, consumer_type: consumer.type). 91 | order("created_at DESC"). 92 | limit(options.fetch(:limit, 30)). 93 | offset(options.fetch(:offset, 0)) 94 | 95 | connections.map do |connection| 96 | Ag::Connection.new({ 97 | id: connection.id, 98 | created_at: connection.created_at, 99 | consumer: Ag::Object.new(connection.consumer_type, connection.consumer_id), 100 | producer: Ag::Object.new(connection.producer_type, connection.producer_id), 101 | }) 102 | end 103 | end 104 | 105 | def timeline(consumer, options = {}) 106 | joins = <<-SQL 107 | INNER JOIN #{Connection.table_name} ON 108 | #{Event.table_name}.producer_id = #{Connection.table_name}.producer_id AND 109 | #{Event.table_name}.producer_type = #{Connection.table_name}.producer_type 110 | SQL 111 | events = Event. 112 | select("#{Event.table_name}.*"). 113 | joins(joins). 114 | where("#{Connection.table_name}.consumer_id = :consumer_id", consumer_id: consumer.id). 115 | where("#{Connection.table_name}.consumer_type = :consumer_type", consumer_type: consumer.type). 116 | order("#{Event.table_name}.created_at DESC"). 117 | limit(options.fetch(:limit, 30)). 118 | offset(options.fetch(:offset, 0)) 119 | 120 | events.map do |event| 121 | Ag::Event.new({ 122 | id: event.id, 123 | created_at: event.created_at, 124 | producer: Ag::Object.new(event.producer_type, event.producer_id), 125 | object: Ag::Object.new(event.object_type, event.object_id), 126 | verb: event.verb, 127 | }) 128 | end 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/ag/adapters/sequel_pull_compact.rb: -------------------------------------------------------------------------------- 1 | require "sequel" 2 | 3 | module Ag 4 | module Adapters 5 | # Adapter that uses the minimum amount of writes while still allowing full 6 | # historic assembly of timelines. This comes at the cost of slower reads. 7 | class SequelPullCompact 8 | def self.dehydrate(object) 9 | [object.type, object.id].join(":") 10 | end 11 | 12 | def self.hydrate(id) 13 | Ag::Object.new(*id.split(":")) 14 | end 15 | 16 | def initialize(db) 17 | @db = db 18 | end 19 | 20 | def connect(consumer, producer) 21 | created_at = Time.now.utc 22 | id = @db[:connections].insert({ 23 | consumer_id: self.class.dehydrate(consumer), 24 | producer_id: self.class.dehydrate(producer), 25 | created_at: created_at, 26 | }) 27 | 28 | Connection.new({ 29 | id: id, 30 | created_at: created_at, 31 | consumer: consumer, 32 | producer: producer, 33 | }) 34 | end 35 | 36 | def produce(event) 37 | created_at = event.created_at || Time.now.utc 38 | id = @db[:events].insert({ 39 | producer_id: self.class.dehydrate(event.producer), 40 | object_id: self.class.dehydrate(event.object), 41 | verb: event.verb, 42 | created_at: created_at, 43 | }) 44 | 45 | Ag::Event.new({ 46 | id: id, 47 | created_at: created_at, 48 | producer: event.producer, 49 | object: event.object, 50 | verb: event.verb, 51 | }) 52 | end 53 | 54 | def connected?(consumer, producer) 55 | statement = <<-SQL 56 | SELECT 57 | 1 58 | FROM 59 | connections 60 | WHERE 61 | consumer_id = :consumer_id AND 62 | producer_id = :producer_id 63 | LIMIT 1 64 | SQL 65 | 66 | binds = { 67 | consumer_id: self.class.dehydrate(consumer), 68 | producer_id: self.class.dehydrate(producer), 69 | } 70 | 71 | !@db[statement, binds].first.nil? 72 | end 73 | 74 | def consumers(producer, options = {}) 75 | statement = <<-SQL 76 | SELECT 77 | id, created_at, consumer_id, producer_id 78 | FROM 79 | connections 80 | WHERE 81 | producer_id = :producer_id 82 | ORDER BY 83 | id DESC 84 | LIMIT :limit 85 | OFFSET :offset 86 | SQL 87 | 88 | binds = { 89 | producer_id: self.class.dehydrate(producer), 90 | limit: options.fetch(:limit, 30), 91 | offset: options.fetch(:offset, 0), 92 | } 93 | 94 | @db[statement, binds].to_a.map { |row| 95 | Connection.new({ 96 | id: row[:id], 97 | created_at: row[:created_at], 98 | consumer: self.class.hydrate(row[:consumer_id]), 99 | producer: self.class.hydrate(row[:producer_id]), 100 | }) 101 | } 102 | end 103 | 104 | def producers(consumer, options = {}) 105 | statement = <<-SQL 106 | SELECT 107 | id, created_at, consumer_id, producer_id 108 | FROM 109 | connections 110 | WHERE 111 | consumer_id = :consumer_id 112 | ORDER BY 113 | id DESC 114 | LIMIT :limit 115 | OFFSET :offset 116 | SQL 117 | 118 | binds = { 119 | consumer_id: self.class.dehydrate(consumer), 120 | limit: options.fetch(:limit, 30), 121 | offset: options.fetch(:offset, 0), 122 | } 123 | 124 | @db[statement, binds].to_a.map { |row| 125 | Connection.new({ 126 | id: row[:id], 127 | created_at: row[:created_at], 128 | consumer: self.class.hydrate(row[:consumer_id]), 129 | producer: self.class.hydrate(row[:producer_id]), 130 | }) 131 | } 132 | end 133 | 134 | def timeline(consumer, options = {}) 135 | statement = <<-SQL 136 | SELECT 137 | e.* 138 | FROM 139 | events e 140 | INNER JOIN 141 | connections c ON 142 | e.producer_id = c.producer_id 143 | WHERE 144 | c.consumer_id = :consumer_id 145 | ORDER BY 146 | e.created_at DESC 147 | LIMIT :limit 148 | OFFSET :offset 149 | SQL 150 | 151 | binds = { 152 | consumer_id: self.class.dehydrate(consumer), 153 | limit: options.fetch(:limit, 30), 154 | offset: options.fetch(:offset, 0), 155 | } 156 | 157 | @db[statement, binds].to_a.map { |row| 158 | Ag::Event.new({ 159 | id: row[:id], 160 | created_at: row[:created_at], 161 | producer: self.class.hydrate(row[:producer_id]), 162 | object: self.class.hydrate(row[:object_id]), 163 | verb: row[:verb], 164 | }) 165 | } 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/ag/adapters/sequel_pull.rb: -------------------------------------------------------------------------------- 1 | require "sequel" 2 | 3 | module Ag 4 | module Adapters 5 | # Adapter that uses the minimum amount of writes while still allowing full 6 | # historic assembly of timelines. This comes at the cost of slower reads. 7 | class SequelPull 8 | def initialize(db) 9 | @db = db 10 | end 11 | 12 | def connect(consumer, producer) 13 | created_at = Time.now.utc 14 | id = @db[:connections].insert({ 15 | consumer_id: consumer.id, 16 | consumer_type: consumer.type, 17 | producer_id: producer.id, 18 | producer_type: producer.type, 19 | created_at: created_at, 20 | }) 21 | 22 | Connection.new({ 23 | id: id, 24 | created_at: created_at, 25 | consumer: consumer, 26 | producer: producer, 27 | }) 28 | end 29 | 30 | def produce(event) 31 | created_at = Time.now.utc 32 | id = @db[:events].insert({ 33 | producer_type: event.producer.type, 34 | producer_id: event.producer.id, 35 | object_type: event.object.type, 36 | object_id: event.object.id, 37 | verb: event.verb, 38 | created_at: created_at, 39 | }) 40 | 41 | Ag::Event.new({ 42 | id: id, 43 | created_at: created_at, 44 | producer: event.producer, 45 | object: event.object, 46 | verb: event.verb, 47 | }) 48 | end 49 | 50 | def connected?(consumer, producer) 51 | statement = <<-SQL 52 | SELECT 53 | 1 54 | FROM 55 | connections 56 | WHERE 57 | consumer_id = :consumer_id AND 58 | producer_id = :producer_id 59 | LIMIT 1 60 | SQL 61 | 62 | binds = { 63 | consumer_id: consumer.id, 64 | consumer_type: consumer.type, 65 | producer_id: producer.id, 66 | producer_type: producer.type, 67 | } 68 | 69 | !@db[statement, binds].first.nil? 70 | end 71 | 72 | def consumers(producer, options = {}) 73 | statement = <<-SQL 74 | SELECT 75 | id, created_at, consumer_id, consumer_type, producer_id, producer_type 76 | FROM 77 | connections 78 | WHERE 79 | producer_id = :producer_id AND 80 | producer_type = :producer_type 81 | ORDER BY 82 | id DESC 83 | LIMIT :limit 84 | OFFSET :offset 85 | SQL 86 | 87 | binds = { 88 | producer_id: producer.id, 89 | producer_type: producer.type, 90 | limit: options.fetch(:limit, 30), 91 | offset: options.fetch(:offset, 0), 92 | } 93 | 94 | @db[statement, binds].to_a.map { |row| 95 | Connection.new({ 96 | id: row[:id], 97 | created_at: row[:created_at], 98 | consumer: Object.new(row[:consumer_type], row[:consumer_id]), 99 | producer: Object.new(row[:producer_type], row[:producer_id]), 100 | }) 101 | } 102 | end 103 | 104 | def producers(consumer, options = {}) 105 | statement = <<-SQL 106 | SELECT 107 | id, created_at, consumer_id, consumer_type, producer_id, producer_type 108 | FROM 109 | connections 110 | WHERE 111 | consumer_id = :consumer_id AND 112 | consumer_type = :consumer_type 113 | ORDER BY 114 | id DESC 115 | LIMIT :limit 116 | OFFSET :offset 117 | SQL 118 | 119 | binds = { 120 | consumer_id: consumer.id, 121 | consumer_type: consumer.type, 122 | limit: options.fetch(:limit, 30), 123 | offset: options.fetch(:offset, 0), 124 | } 125 | 126 | @db[statement, binds].to_a.map { |row| 127 | Connection.new({ 128 | id: row[:id], 129 | created_at: row[:created_at], 130 | consumer: Object.new(row[:consumer_type], row[:consumer_id]), 131 | producer: Object.new(row[:producer_type], row[:producer_id]), 132 | }) 133 | } 134 | end 135 | 136 | def timeline(consumer, options = {}) 137 | statement = <<-SQL 138 | SELECT 139 | e.* 140 | FROM 141 | events e 142 | INNER JOIN 143 | connections c ON 144 | e.producer_id = c.producer_id AND 145 | e.producer_type = c.producer_type 146 | WHERE 147 | c.consumer_id = :consumer_id AND 148 | c.consumer_type = :consumer_type 149 | ORDER BY 150 | e.created_at DESC 151 | LIMIT :limit 152 | OFFSET :offset 153 | SQL 154 | 155 | binds = { 156 | consumer_id: consumer.id, 157 | consumer_type: consumer.type, 158 | limit: options.fetch(:limit, 30), 159 | offset: options.fetch(:offset, 0), 160 | } 161 | 162 | @db[statement, binds].to_a.map { |row| 163 | Ag::Event.new({ 164 | id: row[:id], 165 | created_at: row[:created_at], 166 | producer: Ag::Object.new(row[:producer_type], row[:producer_id]), 167 | object: Ag::Object.new(row[:object_type], row[:object_id]), 168 | verb: row[:verb], 169 | }) 170 | } 171 | end 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/ag/adapters/sequel_push.rb: -------------------------------------------------------------------------------- 1 | require "sequel/core" 2 | 3 | module Ag 4 | module Adapters 5 | # Adapter that uses the maximum amount of writes in order to make 6 | # reading faster. 7 | class SequelPush 8 | def initialize(db) 9 | @db = db 10 | end 11 | 12 | def connect(consumer, producer) 13 | created_at = Time.now.utc 14 | id = @db[:connections].insert({ 15 | consumer_id: consumer.id, 16 | consumer_type: consumer.type, 17 | producer_id: producer.id, 18 | producer_type: producer.type, 19 | created_at: created_at, 20 | }) 21 | 22 | Connection.new({ 23 | id: id, 24 | created_at: created_at, 25 | consumer: consumer, 26 | producer: producer, 27 | }) 28 | end 29 | 30 | def produce(event) 31 | created_at = Time.now.utc 32 | 33 | id = @db[:events].insert({ 34 | producer_type: event.producer.type, 35 | producer_id: event.producer.id, 36 | object_type: event.object.type, 37 | object_id: event.object.id, 38 | verb: event.verb, 39 | created_at: created_at, 40 | }) 41 | 42 | result = Ag::Event.new({ 43 | id: id, 44 | created_at: created_at, 45 | producer: event.producer, 46 | object: event.object, 47 | verb: event.verb, 48 | }) 49 | 50 | # FIXME: don't want to transaction around this and event insert because 51 | # this could take a while and long transactions are terrible, but do 52 | # need to do some failure handling in here 53 | each_consumer(result.producer) do |consumer| 54 | @db[:timelines].insert({ 55 | consumer_id: consumer.id, 56 | consumer_type: consumer.type, 57 | event_id: result.id, 58 | created_at: result.created_at, 59 | }) 60 | end 61 | 62 | result 63 | end 64 | 65 | def connected?(consumer, producer) 66 | statement = <<-SQL 67 | SELECT 68 | 1 69 | FROM 70 | connections 71 | WHERE 72 | consumer_id = :consumer_id AND 73 | producer_id = :producer_id 74 | LIMIT 1 75 | SQL 76 | 77 | binds = { 78 | consumer_id: consumer.id, 79 | consumer_type: consumer.type, 80 | producer_id: producer.id, 81 | producer_type: producer.type, 82 | } 83 | 84 | !@db[statement, binds].first.nil? 85 | end 86 | 87 | def consumers(producer, options = {}) 88 | statement = <<-SQL 89 | SELECT 90 | id, created_at, consumer_id, consumer_type, producer_id, producer_type 91 | FROM 92 | connections 93 | WHERE 94 | producer_id = :producer_id AND 95 | producer_type = :producer_type 96 | ORDER BY 97 | id DESC 98 | LIMIT :limit 99 | OFFSET :offset 100 | SQL 101 | 102 | binds = { 103 | producer_id: producer.id, 104 | producer_type: producer.type, 105 | limit: options.fetch(:limit, 30), 106 | offset: options.fetch(:offset, 0), 107 | } 108 | 109 | @db[statement, binds].to_a.map { |row| 110 | Connection.new({ 111 | id: row[:id], 112 | created_at: row[:created_at], 113 | consumer: Object.new(row[:consumer_type], row[:consumer_id]), 114 | producer: Object.new(row[:producer_type], row[:producer_id]), 115 | }) 116 | } 117 | end 118 | 119 | def producers(consumer, options = {}) 120 | statement = <<-SQL 121 | SELECT 122 | id, created_at, consumer_id, consumer_type, producer_id, producer_type 123 | FROM 124 | connections 125 | WHERE 126 | consumer_id = :consumer_id AND 127 | consumer_type = :consumer_type 128 | ORDER BY 129 | id DESC 130 | LIMIT :limit 131 | OFFSET :offset 132 | SQL 133 | 134 | binds = { 135 | consumer_id: consumer.id, 136 | consumer_type: consumer.type, 137 | limit: options.fetch(:limit, 30), 138 | offset: options.fetch(:offset, 0), 139 | } 140 | 141 | @db[statement, binds].to_a.map { |row| 142 | Connection.new({ 143 | id: row[:id], 144 | created_at: row[:created_at], 145 | consumer: Object.new(row[:consumer_type], row[:consumer_id]), 146 | producer: Object.new(row[:producer_type], row[:producer_id]), 147 | }) 148 | } 149 | end 150 | 151 | def timeline(consumer, options = {}) 152 | statement = <<-SQL 153 | SELECT 154 | e.id, e.created_at, 155 | e.object_type, e.object_id, 156 | e.producer_type, e.producer_id 157 | FROM 158 | events e 159 | INNER JOIN 160 | timelines t ON e.id = t.event_id 161 | WHERE 162 | t.consumer_id = :consumer_id AND 163 | t.consumer_type = :consumer_type 164 | ORDER BY 165 | t.created_at DESC 166 | LIMIT :limit 167 | OFFSET :offset 168 | SQL 169 | 170 | binds = { 171 | consumer_id: consumer.id, 172 | consumer_type: consumer.type, 173 | limit: options.fetch(:limit, 30), 174 | offset: options.fetch(:offset, 0), 175 | } 176 | 177 | @db[statement, binds].to_a.map { |row| 178 | Ag::Event.new({ 179 | id: row[:id], 180 | created_at: row[:created_at], 181 | producer: Ag::Object.new(row[:producer_type], row[:producer_id]), 182 | object: Ag::Object.new(row[:object_type], row[:object_id]), 183 | verb: row[:verb], 184 | }) 185 | } 186 | end 187 | 188 | private 189 | 190 | # FIXME: single query is terrible, need to do batches 191 | def each_consumer(producer) 192 | statement = <<-SQL 193 | SELECT 194 | consumer_id, consumer_type 195 | FROM 196 | connections 197 | WHERE 198 | producer_id = :producer_id AND 199 | producer_type = :producer_type 200 | ORDER BY 201 | id ASC 202 | SQL 203 | 204 | binds = { 205 | producer_id: producer.id, 206 | producer_type: producer.type, 207 | } 208 | 209 | @db[statement, binds].each do |row| 210 | yield Ag::Object.new(row[:consumer_type], row[:consumer_id]) 211 | end 212 | end 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/ag/spec/adapter.rb: -------------------------------------------------------------------------------- 1 | module Ag 2 | module Spec 3 | module Adapter 4 | def test_connect 5 | consumer = Ag::Object.new("User", "1") 6 | producer = Ag::Object.new("User", "2") 7 | 8 | adapter.connect(consumer, producer) 9 | 10 | assert_equal consumer, adapter.consumers(producer).map(&:consumer).first 11 | assert_equal producer, adapter.producers(consumer).map(&:producer).first 12 | 13 | connection = adapter.consumers(producer).first 14 | refute_nil connection 15 | assert_equal consumer.id, connection.consumer.id 16 | assert_equal consumer.type, connection.consumer.type 17 | assert_equal producer.id, connection.producer.id 18 | assert_equal producer.type, connection.producer.type 19 | assert_in_delta Time.now.utc, connection.created_at, 1 20 | end 21 | 22 | def test_produce 23 | producer = Ag::Object.new("User", "1") 24 | consumer = Ag::Object.new("User", "2") 25 | adapter.connect(consumer, producer) 26 | 27 | object = Ag::Object.new("User", "3") 28 | event = Ag::Event.new({ 29 | producer: producer, 30 | object: object, 31 | verb: "follow", 32 | }) 33 | 34 | result = adapter.produce(event) 35 | 36 | event = adapter.timeline(consumer).first 37 | assert_equal event.id, result.id 38 | assert_equal producer.id, event.producer.id 39 | assert_equal producer.type, event.producer.type 40 | assert_equal object.id, event.object.id 41 | assert_equal object.type, event.object.type 42 | assert_in_delta Time.now.utc, event.created_at, 1 43 | end 44 | 45 | def test_connected 46 | consumer = Ag::Object.new("User", "1") 47 | producer = Ag::Object.new("User", "2") 48 | adapter.connect(consumer, producer) 49 | 50 | assert_equal true, adapter.connected?(consumer, producer) 51 | assert_equal false, adapter.connected?(producer, consumer) 52 | end 53 | 54 | def test_consumers 55 | consumer1 = Ag::Object.new("User", "1") 56 | consumer2 = Ag::Object.new("User", "2") 57 | consumer3 = Ag::Object.new("User", "3") 58 | producer = Ag::Object.new("User", "4") 59 | adapter.connect(consumer1, producer) 60 | adapter.connect(consumer2, producer) 61 | 62 | consumers = adapter.consumers(producer) 63 | assert_equal 2, consumers.size 64 | assert_equal "2", consumers[0].consumer.id 65 | assert_equal "1", consumers[1].consumer.id 66 | end 67 | 68 | def test_consumers_limit 69 | producer = Ag::Object.new("User", "99") 70 | consumers = (0..9).to_a.map { |n| 71 | Ag::Object.new("User", n.to_s).tap { |consumer| 72 | adapter.connect consumer, producer 73 | } 74 | } 75 | assert_equal 5, adapter.consumers(producer, limit: 5).size 76 | assert_equal consumers[5..9].reverse, 77 | adapter.consumers(producer, limit: 5).map(&:consumer) 78 | 79 | assert_equal 1, adapter.consumers(producer, limit: 1).size 80 | assert_equal [consumers[9]], 81 | adapter.consumers(producer, limit: 1).map(&:consumer) 82 | end 83 | 84 | def test_consumers_offset 85 | producer = Ag::Object.new("User", "99") 86 | consumers = (0..9).to_a.map { |n| 87 | Ag::Object.new("User", n.to_s).tap { |consumer| 88 | adapter.connect consumer, producer 89 | } 90 | } 91 | assert_equal consumers[0..4].reverse, 92 | adapter.consumers(producer, offset: 5).map(&:consumer) 93 | end 94 | 95 | def test_producers 96 | consumer1 = Ag::Object.new("User", "1") 97 | consumer2 = Ag::Object.new("User", "2") 98 | producer1 = Ag::Object.new("User", "3") 99 | producer2 = Ag::Object.new("User", "4") 100 | producer3 = Ag::Object.new("User", "5") 101 | adapter.connect(consumer1, producer1) 102 | adapter.connect(consumer1, producer2) 103 | adapter.connect(consumer2, producer3) 104 | 105 | producers = adapter.producers(consumer1) 106 | assert_equal 2, producers.size 107 | assert_equal "4", producers[0].producer.id 108 | assert_equal "3", producers[1].producer.id 109 | end 110 | 111 | def test_producers_limit 112 | consumer = Ag::Object.new("User", "99") 113 | producers = (0..9).to_a.map { |n| 114 | Ag::Object.new("User", n.to_s).tap { |producer| 115 | adapter.connect consumer, producer 116 | } 117 | } 118 | assert_equal 5, adapter.producers(consumer, limit: 5).size 119 | assert_equal producers[5..9].reverse, 120 | adapter.producers(consumer, limit: 5).map(&:producer) 121 | 122 | assert_equal 1, adapter.producers(consumer, limit: 1).size 123 | assert_equal [producers[9]], 124 | adapter.producers(consumer, limit: 1).map(&:producer) 125 | end 126 | 127 | def test_producers_offset 128 | consumer = Ag::Object.new("User", "99") 129 | producers = (0..9).to_a.map { |n| 130 | Ag::Object.new("User", n.to_s).tap { |producer| 131 | adapter.connect consumer, producer 132 | } 133 | } 134 | assert_equal producers[0..4].reverse, 135 | adapter.producers(consumer, offset: 5).map(&:producer) 136 | end 137 | 138 | def test_timeline 139 | john = Ag::Object.new("User", "1") 140 | steve = Ag::Object.new("User", "2") 141 | presentation = Ag::Object.new("Presentation", "1") 142 | adapter.connect john, steve 143 | adapter.produce Ag::Event.new(producer: steve, object: presentation, verb: "publish") 144 | 145 | events = adapter.timeline(john) 146 | assert_equal 1, events.size 147 | end 148 | 149 | def test_timeline_limit 150 | john = Ag::Object.new("User", "1") 151 | steve = Ag::Object.new("User", "2") 152 | adapter.connect john, steve 153 | 154 | presentations = (0..9).to_a.map { |n| 155 | Ag::Object.new("Presentation", n.to_s) 156 | } 157 | 158 | presentations.each do |presentation| 159 | adapter.produce Ag::Event.new({ 160 | producer: steve, 161 | object: presentation, 162 | verb: "publish", 163 | }) 164 | end 165 | 166 | events = adapter.timeline(john, limit: 5) 167 | assert_equal 5, events.size 168 | assert_equal presentations[5..9].reverse, events.map(&:object) 169 | end 170 | 171 | def test_timeline_offset 172 | john = Ag::Object.new("User", "1") 173 | steve = Ag::Object.new("User", "2") 174 | adapter.connect john, steve 175 | 176 | presentations = (0..9).to_a.map { |n| 177 | Ag::Object.new("Presentation", n.to_s) 178 | } 179 | 180 | presentations.each do |presentation| 181 | adapter.produce Ag::Event.new({ 182 | producer: steve, 183 | object: presentation, 184 | verb: "publish", 185 | }) 186 | end 187 | 188 | events = adapter.timeline(john, offset: 5) 189 | assert_equal 5, events.size 190 | assert_equal presentations[0..4].reverse, events.map(&:object) 191 | end 192 | end 193 | end 194 | end 195 | --------------------------------------------------------------------------------