hyper-operation 1.0.alpha1.5 → 1.0.alpha1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyperstack
4
+ module ConnectionAdapter
5
+ module ActiveRecord
6
+ module AutoCreate
7
+ def table_exists?
8
+ # works with both rails 4 and 5 without deprecation warnings
9
+ if connection.respond_to?(:data_sources)
10
+ connection.data_sources.include?(table_name)
11
+ else
12
+ connection.tables.include?(table_name)
13
+ end
14
+ end
15
+
16
+ def needs_init?
17
+ Hyperstack.transport != :none && Hyperstack.on_server? && !table_exists?
18
+ end
19
+
20
+ def create_table(*args, &block)
21
+ connection.create_table(table_name, *args, &block) if needs_init?
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auto_create'
4
+
5
+ module Hyperstack
6
+ module ConnectionAdapter
7
+ module ActiveRecord
8
+ class Connection < ::ActiveRecord::Base
9
+ extend AutoCreate
10
+
11
+ self.table_name = 'hyperstack_connections'
12
+
13
+ do_not_synchronize
14
+
15
+ has_many :messages,
16
+ foreign_key: 'connection_id',
17
+ class_name: 'Hyperstack::ConnectionAdapter::ActiveRecord::QueuedMessage',
18
+ dependent: :destroy
19
+
20
+ scope :expired,
21
+ -> { where('expires_at IS NOT NULL AND expires_at < ?', Time.current) }
22
+ scope :pending_for,
23
+ ->(channel) { where(channel: channel).where('session IS NOT NULL') }
24
+ scope :inactive,
25
+ -> { where('session IS NULL AND refresh_at < ?', Time.current) }
26
+
27
+ before_create do
28
+ if session
29
+ self.expires_at = Time.current + transport.expire_new_connection_in
30
+ elsif transport.refresh_channels_every != :never
31
+ self.refresh_at = Time.current + transport.refresh_channels_every
32
+ end
33
+ end
34
+
35
+ class << self
36
+ def needs_refresh?
37
+ exists?(['refresh_at IS NOT NULL AND refresh_at < ?', Time.current])
38
+ end
39
+ end
40
+
41
+ def transport
42
+ Hyperstack::Connection.transport
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'auto_create'
4
+
5
+ module Hyperstack
6
+ module ConnectionAdapter
7
+ module ActiveRecord
8
+ class QueuedMessage < ::ActiveRecord::Base
9
+ extend AutoCreate
10
+
11
+ self.table_name = 'hyperstack_queued_messages'
12
+
13
+ do_not_synchronize
14
+
15
+ serialize :data
16
+
17
+ belongs_to :hyperstack_connection,
18
+ class_name: 'Hyperstack::ConnectionAdapter::ActiveRecord::Connection',
19
+ foreign_key: 'connection_id',
20
+ optional: true
21
+
22
+ scope :for_session,
23
+ ->(session) { joins(:hyperstack_connection).where('session = ?', session) }
24
+
25
+ # For simplicity we use QueuedMessage with connection_id 0
26
+ # to store the current path which is used by consoles to
27
+ # communicate back to the server. The belongs_to connection
28
+ # therefore must be optional.
29
+
30
+ default_scope { where('connection_id IS NULL OR connection_id != 0') }
31
+
32
+ def self.root_path=(path)
33
+ unscoped.find_or_create_by(connection_id: 0).update(data: path)
34
+ end
35
+
36
+ def self.root_path
37
+ unscoped.find_or_create_by(connection_id: 0).data
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'redis/connection'
4
+ require_relative 'redis/queued_message'
5
+
6
+ module Hyperstack
7
+ module ConnectionAdapter
8
+ module Redis
9
+ class << self
10
+ def transport
11
+ Hyperstack::Connection.transport
12
+ end
13
+
14
+ def active
15
+ if Hyperstack.on_server?
16
+ Connection.expired.each(&:destroy)
17
+ refresh_connections if Connection.needs_refresh?
18
+ end
19
+
20
+ Connection.all.map(&:channel).uniq
21
+ end
22
+
23
+ def open(channel, session = nil, root_path = nil)
24
+ self.root_path = root_path
25
+
26
+ Connection.find_or_create_by(channel: channel, session: session)
27
+ end
28
+
29
+ def send_to_channel(channel, data)
30
+ Connection.pending_for(channel).each do |connection|
31
+ QueuedMessage.create(connection_id: connection.id, data: data)
32
+ end
33
+
34
+ transport.send_data(channel, data) if Connection.exists?(channel: channel, session: nil)
35
+ end
36
+
37
+ def read(session, root_path)
38
+ self.root_path = root_path
39
+
40
+ Connection.where(session: session).each do |connection|
41
+ connection.update(expires_at: Time.current + transport.expire_polled_connection_in)
42
+ end
43
+
44
+ messages = QueuedMessage.for_session(session)
45
+ data = messages.map(&:data)
46
+ messages.each(&:destroy)
47
+ data
48
+ end
49
+
50
+ def connect_to_transport(channel, session, root_path)
51
+ self.root_path = root_path
52
+
53
+ if (connection = Connection.find_by(channel: channel, session: session))
54
+ messages = connection.messages.map(&:data)
55
+ connection.destroy
56
+ else
57
+ messages = []
58
+ end
59
+
60
+ open(channel)
61
+
62
+ messages
63
+ end
64
+
65
+ def disconnect(channel)
66
+ Connection.find_by(channel: channel, session: nil).destroy
67
+ end
68
+
69
+ def root_path=(path)
70
+ QueuedMessage.root_path = path if path
71
+ end
72
+
73
+ def root_path
74
+ QueuedMessage.root_path
75
+ rescue
76
+ nil
77
+ end
78
+
79
+ def refresh_connections
80
+ refresh_started_at = Time.current
81
+ channels = transport.refresh_channels
82
+ next_refresh = refresh_started_at + transport.refresh_channels_every
83
+
84
+ channels.each do |channel|
85
+ connection = Connection.find_by(channel: channel, session: nil)
86
+ connection.update(refresh_at: next_refresh) if connection
87
+ end
88
+
89
+ Connection.inactive.each(&:destroy)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'redis_record'
4
+
5
+ module Hyperstack
6
+ module ConnectionAdapter
7
+ module Redis
8
+ class Connection < RedisRecord::Base
9
+ self.table_name = 'hyperstack:connections'
10
+ self.column_names = %w[id channel session created_at expires_at refresh_at].freeze
11
+
12
+ attr_accessor(*column_names.map(&:to_sym))
13
+
14
+ class << self
15
+ def transport
16
+ Hyperstack::Connection.transport
17
+ end
18
+
19
+ def create(opts = {})
20
+ opts.tap do |hash|
21
+ if opts[:session]
22
+ hash[:expires_at] = (Time.current + transport.expire_new_connection_in)
23
+ elsif transport.refresh_channels_every != :never
24
+ hash[:refresh_at] = (Time.current + transport.refresh_channels_every)
25
+ end
26
+
27
+ hash[:created_at] = Time.current
28
+ end.to_a.flatten
29
+
30
+ super(opts)
31
+ end
32
+
33
+ def inactive
34
+ scope { |id| id if inactive?(id) }
35
+ end
36
+
37
+ def inactive?(id)
38
+ get_dejsonized_attribute(id, :session).blank? &&
39
+ get_dejsonized_attribute(id, :refresh_at).present? &&
40
+ Time.zone.parse(get_dejsonized_attribute(id, :refresh_at)) < Time.current
41
+ end
42
+
43
+ def expired
44
+ scope { |id| id if expired?(id) }
45
+ end
46
+
47
+ def expired?(id)
48
+ get_dejsonized_attribute(id, :expires_at).present? &&
49
+ Time.zone.parse(get_dejsonized_attribute(id, :expires_at)) < Time.current
50
+ end
51
+
52
+ def pending_for(channel)
53
+ scope { |id| id if pending_for?(id, channel) }
54
+ end
55
+
56
+ def pending_for?(id, channel)
57
+ get_dejsonized_attribute(id, :session).present? &&
58
+ get_dejsonized_attribute(id, :channel) == channel
59
+ end
60
+
61
+ def needs_refresh?
62
+ scope { |id| id if needs_refresh(id) }.any?
63
+ end
64
+
65
+ def needs_refresh(id)
66
+ get_dejsonized_attribute(id, :refresh_at).present? &&
67
+ Time.zone.parse(get_dejsonized_attribute(id, :refresh_at)) < Time.current
68
+ end
69
+ end
70
+
71
+ def messages
72
+ QueuedMessage.where(connection_id: id)
73
+ end
74
+
75
+ %i[created_at expires_at refresh_at].each do |attr|
76
+ define_method(attr) do
77
+ value = instance_variable_get(:"@#{attr}")
78
+
79
+ value.is_a?(Time) ? value : Time.zone.parse(value)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'redis_record'
4
+
5
+ module Hyperstack
6
+ module ConnectionAdapter
7
+ module Redis
8
+ class QueuedMessage < RedisRecord::Base
9
+ self.table_name = 'hyperstack:queued_messages'
10
+ self.column_names = %w[id data connection_id].freeze
11
+
12
+ attr_accessor(*column_names.map(&:to_sym))
13
+
14
+ class << self
15
+ def for_session(session)
16
+ Connection.where(session: session).map(&:messages).flatten
17
+ end
18
+
19
+ def root_path=(path)
20
+ find_or_create_by(connection_id: 0).update(data: path)
21
+ end
22
+
23
+ def root_path
24
+ find_or_create_by(connection_id: 0).data
25
+ end
26
+ end
27
+
28
+ def connection
29
+ Connection.find(connection_id)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hyperstack
4
+ module ConnectionAdapter
5
+ module Redis
6
+ module RedisRecord
7
+ class Base
8
+ class << self
9
+ attr_accessor :table_name, :column_names
10
+
11
+ def client
12
+ @client ||= ::Redis.new(url: Hyperstack.connection[:redis_url])
13
+ end
14
+
15
+ def scope(&block)
16
+ ids = client.smembers(table_name)
17
+
18
+ ids = ids.map(&block) if block
19
+
20
+ ids.compact.map { |id| instantiate(id) }
21
+ end
22
+
23
+ def all
24
+ scope
25
+ end
26
+
27
+ def first
28
+ id = client.smembers(table_name).first
29
+
30
+ instantiate(id)
31
+ end
32
+
33
+ def last
34
+ id = client.smembers(table_name).last
35
+
36
+ instantiate(id)
37
+ end
38
+
39
+ def find(id)
40
+ return unless client.smembers(table_name).include?(id)
41
+
42
+ instantiate(id)
43
+ end
44
+
45
+ def find_by(opts)
46
+ found = nil
47
+
48
+ client.smembers(table_name).each do |id|
49
+ unless opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
50
+ found = instantiate(id)
51
+ break
52
+ end
53
+ end
54
+
55
+ found
56
+ end
57
+
58
+ def find_or_create_by(opts = {})
59
+ if (existing = find_by(opts))
60
+ existing
61
+ else
62
+ create(opts)
63
+ end
64
+ end
65
+
66
+ def where(opts = {})
67
+ scope do |id|
68
+ unless opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
69
+ id
70
+ end
71
+ end
72
+ end
73
+
74
+ def exists?(opts = {})
75
+ !!client.smembers(table_name).detect do |id|
76
+ !opts.map { |k, v| get_dejsonized_attribute(id, k) == v }.include?(false)
77
+ end
78
+ end
79
+
80
+ def create(opts = {})
81
+ record = new({ id: SecureRandom.uuid }.merge(opts))
82
+
83
+ record.save
84
+
85
+ record
86
+ end
87
+
88
+ def destroy_all
89
+ all.each(&:destroy)
90
+
91
+ true
92
+ end
93
+
94
+ def jsonize_attributes(attrs)
95
+ attrs.map do |attr, value|
96
+ [attr, value.to_json]
97
+ end.to_h
98
+ end
99
+
100
+ def dejsonize_attributes(attrs)
101
+ attrs.map do |attr, value|
102
+ [attr, value && JSON.parse(value)]
103
+ end.to_h
104
+ end
105
+
106
+ protected
107
+
108
+ def instantiate(id)
109
+ new(dejsonize_attributes(client.hgetall("#{table_name}:#{id}")))
110
+ end
111
+
112
+ def get_dejsonized_attribute(id, attr)
113
+ value = client.hget("#{table_name}:#{id}", attr)
114
+ JSON.parse(value) if value
115
+ end
116
+ end
117
+
118
+ def initialize(opts = {})
119
+ opts.each { |k, v| send(:"#{k}=", v) }
120
+ end
121
+
122
+ def save
123
+ self.class.client.hmset("#{table_name}:#{id}", *self.class.jsonize_attributes(attributes))
124
+
125
+ unless self.class.client.smembers(table_name).include?(id)
126
+ self.class.client.sadd(table_name, id)
127
+ end
128
+
129
+ true
130
+ end
131
+
132
+ def update(opts = {})
133
+ opts.each { |k, v| send(:"#{k}=", v) }
134
+ save
135
+ end
136
+
137
+ def destroy
138
+ self.class.client.srem(table_name, id)
139
+
140
+ self.class.client.hdel("#{table_name}:#{id}", attributes.keys)
141
+
142
+ true
143
+ end
144
+
145
+ def attributes
146
+ self.class.column_names.map do |column_name|
147
+ [column_name, instance_variable_get("@#{column_name}")]
148
+ end.to_h
149
+ end
150
+
151
+ def table_name
152
+ self.class.table_name
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end