estore 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ffaed9c9b3874fa4950a194ebc95a84474f5891
4
- data.tar.gz: aa3d8f02bba1e085557a7fc0567b619d28b97467
3
+ metadata.gz: 18db1ad6f67a5c73ecd75dbe6fc80f5f03b717f2
4
+ data.tar.gz: 47b240aa7ce8fddbe969103718a42a1668575f8d
5
5
  SHA512:
6
- metadata.gz: 1dc0acb4b9d8c53e5c58547cbdf610343c221646444f44341b0365d082e327f32c2c8234ac4efe1b8d7ee4faa3670b4c0abd4add84182cd5668085aa61de584e
7
- data.tar.gz: 329da382ffb6ae45ad9c0033598232a2b757d0461a7071b569f479b1c3b75fc48e1fdb918b48eb7672b170732ef8659f5550b2ccabac3ed6cb48c807810f74db
6
+ metadata.gz: b5d4d2f91f7e7c2296684ac876e11b9c3959db6988e5b48cf0a8e5888723cd048acf68b14ebe159b0e9c2d4494030d2179912ef1777c6a4b878b5c85fbebcb60
7
+ data.tar.gz: abaeaa10b0ec1c4d623bddb40550cee7f8d8afe2eddf36f19baea5092548cd5a74b6dd8098329f6d07a5b2626c634e57ae45d3bb846d79868f57dd8e9bb6c035
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Mathieu Ravaux', 'Héctor Ramón']
10
10
  spec.email = ['mathieu.ravaux@gmail.com', 'hector0193@gmail.com']
11
11
  spec.summary = 'An Event Store driver for Ruby'
12
- spec.description = spec.summary
12
+ spec.description = 'TCP driver to read and write events to Event Store'
13
13
  spec.homepage = 'https://github.com/rom-eventstore/estore'
14
14
  spec.license = 'MIT'
15
15
 
@@ -22,6 +22,6 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency 'promise.rb', '~> 0.6.1'
23
23
 
24
24
  spec.add_development_dependency 'bundler'
25
- spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
26
  spec.add_development_dependency 'rubocop', '~> 0.28.0'
27
27
  end
@@ -6,6 +6,5 @@ require 'estore/message_extensions'
6
6
  require 'estore/connection_context'
7
7
  require 'estore/connection'
8
8
  require 'estore/connection/buffer'
9
- require 'estore/connection/commands'
10
- require 'estore/subscription'
11
- require 'estore/catchup_subscription'
9
+ require 'estore/connection/protocol'
10
+ require 'estore/commands'
@@ -0,0 +1,8 @@
1
+ require 'estore/commands/promise'
2
+ require 'estore/commands/base'
3
+ require 'estore/commands/append'
4
+ require 'estore/commands/ping'
5
+ require 'estore/commands/read_batch'
6
+ require 'estore/commands/read_forward'
7
+ require 'estore/commands/subscription'
8
+ require 'estore/commands/catch_up_subscription'
@@ -0,0 +1,56 @@
1
+ module Estore
2
+ module Commands
3
+ class Append
4
+ include Command
5
+
6
+ CONTENT_TYPES = {
7
+ json: 1
8
+ }
9
+
10
+ def initialize(connection, stream, events, options = {})
11
+ super(connection)
12
+ @stream, @events, @options = stream, events, options
13
+ end
14
+
15
+ def call
16
+ msg = WriteEvents.new(
17
+ event_stream_id: @stream,
18
+ expected_version: @options[:expected_version] || -2,
19
+ events: Array(@events).map { |event| new_event(event) },
20
+ require_master: true
21
+ )
22
+
23
+ register!
24
+ write('WriteEvents', msg)
25
+ promise
26
+ end
27
+
28
+ def handle(message, *)
29
+ response = decode(WriteEventsCompleted, message)
30
+
31
+ if response.result != OperationResult::Success
32
+ # TODO: Create custom exceptions
33
+ raise "WriteEvents command failed with uuid #{@uuid}"
34
+ end
35
+
36
+ remove!
37
+ promise.fulfill(response)
38
+ end
39
+
40
+ private
41
+
42
+ def new_event(event)
43
+ uuid = event[:id] || SecureRandom.uuid
44
+ content_type = event.fetch(:content_type, :json)
45
+
46
+ NewEvent.new(
47
+ event_id: Package.encode_uuid(uuid),
48
+ event_type: event[:type],
49
+ data: event[:data],
50
+ data_content_type: CONTENT_TYPES.fetch(content_type, 0),
51
+ metadata_content_type: 1
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,51 @@
1
+ module Estore
2
+ module Commands
3
+ module Command
4
+ attr_reader :uuid
5
+
6
+ def initialize(connection)
7
+ @connection = connection
8
+ @uuid = SecureRandom.uuid
9
+ end
10
+
11
+ def register!
12
+ @connection.register(self)
13
+ end
14
+
15
+ def remove!
16
+ @connection.remove(self)
17
+ end
18
+
19
+ def write(command, message = nil)
20
+ @connection.write(@uuid, command, message)
21
+ end
22
+
23
+ def promise
24
+ @promise ||= Promise.new(@uuid)
25
+ end
26
+
27
+ def decode(type, message)
28
+ type.decode(message)
29
+ rescue => error
30
+ puts "Protobuf decoding error on connection #{object_id}"
31
+ puts type: type, message: message
32
+ puts error.backtrace
33
+ raise error
34
+ end
35
+
36
+ module ReadStreamForward
37
+ def read(stream, from, limit)
38
+ msg = ReadStreamEvents.new(
39
+ event_stream_id: stream,
40
+ from_event_number: from,
41
+ max_count: limit,
42
+ resolve_link_tos: true,
43
+ require_master: false
44
+ )
45
+
46
+ write('ReadStreamEventsForward', msg)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ module Estore
2
+ module Commands
3
+ class CatchUpSubscription < Subscription
4
+ include Command
5
+
6
+ def initialize(connection, stream, from, options = {})
7
+ super(connection, options)
8
+ @stream = stream
9
+ @from = from
10
+ @batch = options[:batch_size]
11
+ @mutex = Mutex.new
12
+ @queue = []
13
+ @caught_up = false
14
+ @last_worker = nil
15
+ end
16
+
17
+ def start
18
+ super
19
+
20
+ # TODO: Think about doing something more clever?
21
+ read = ReadForward.new(@connection, @stream, @from, @batch) do |events|
22
+ @last_worker = Thread.new(@last_worker) do |last_worker|
23
+ last_worker.join if last_worker
24
+ events.each { |event| dispatch(event) }
25
+ end
26
+ end
27
+
28
+ read.call.sync
29
+ @last_worker.join if @last_worker
30
+
31
+ switch_to_live
32
+ end
33
+
34
+ def switch_to_live
35
+ @mutex.synchronize do
36
+ queued_events.each { |event| dispatch(event) }
37
+ @caught_up = true
38
+ end
39
+ end
40
+
41
+ def queued_events
42
+ @queue.find_all { |event| event.original_event_number > @position }
43
+ end
44
+
45
+ def handle(message, type)
46
+ if type == 'StreamEventAppeared'
47
+ event = decode(StreamEventAppeared, message).event
48
+
49
+ unless @caught_up
50
+ @mutex.synchronize do
51
+ @queue << event unless @caught_up
52
+ end
53
+ end
54
+
55
+ dispatch(event) if @caught_up
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ module Estore
2
+ module Commands
3
+ class Ping
4
+ include Command
5
+
6
+ def call
7
+ write('Ping')
8
+ promise
9
+ end
10
+
11
+ def handle(*)
12
+ promise.fulfill('Pong')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ module Estore
2
+ module Commands
3
+ class Promise < ::Promise
4
+ attr_reader :correlation_id
5
+
6
+ def initialize(correlation_id)
7
+ super()
8
+ @correlation_id = correlation_id
9
+ end
10
+
11
+ def wait
12
+ t = Thread.current
13
+ resume = proc { t.wakeup }
14
+ self.then(resume, resume)
15
+ sleep
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ module Estore
2
+ module Commands
3
+ class ReadBatch
4
+ include Command
5
+ include Command::ReadStreamForward
6
+
7
+ def initialize(connection, stream, from, limit)
8
+ super(connection)
9
+ @stream, @from, @limit = stream, from, limit
10
+ end
11
+
12
+ def call
13
+ register!
14
+ read(@stream, @from, @limit)
15
+ promise
16
+ end
17
+
18
+ def handle(message, *)
19
+ remove!
20
+ response = decode(ReadStreamEventsCompleted, message)
21
+ promise.fulfill(Array(response.events))
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ module Estore
2
+ module Commands
3
+ class ReadForward
4
+ include Command
5
+ include Command::ReadStreamForward
6
+
7
+ def initialize(connection, stream, from, batch_size = nil, &block)
8
+ super(connection)
9
+
10
+ @stream = stream
11
+ @from = from
12
+ @batch_size = batch_size || 1000
13
+ @block = block
14
+ @events = []
15
+ end
16
+
17
+ def call
18
+ register!
19
+ read(@stream, @from, @batch_size)
20
+ promise
21
+ end
22
+
23
+ def handle(message, *)
24
+ response = decode(ReadStreamEventsCompleted, message)
25
+ events = Array(response.events)
26
+
27
+ @from += events.size
28
+ read(@stream, @from, @batch_size) unless response.is_end_of_stream
29
+
30
+ @block ? @block.call(events) : @events.push(*events)
31
+
32
+ if response.is_end_of_stream
33
+ remove!
34
+ promise.fulfill(@block ? nil : @events)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module Estore
2
+ module Commands
3
+ class Subscription
4
+ include Command
5
+
6
+ def initialize(connection, stream, options = {})
7
+ super(connection)
8
+ @has_finished = false
9
+ @stream = stream
10
+ @resolve_link_tos = options.fetch(:resolve_link_tos, true)
11
+ end
12
+
13
+ def finished?
14
+ @has_finished
15
+ end
16
+
17
+ def call
18
+ start
19
+ end
20
+
21
+ def start
22
+ raise 'Subscription block not defined' unless @handler
23
+
24
+ msg = SubscribeToStream.new(
25
+ event_stream_id: @stream,
26
+ resolve_link_tos: @resolve_link_tos
27
+ )
28
+
29
+ register!
30
+ write('SubscribeToStream', msg)
31
+ end
32
+
33
+ def close
34
+ write('UnsubscribeFromStream', UnsubscribeFromStream.new)
35
+ remove!
36
+ end
37
+
38
+ def on_event(&block)
39
+ @handler = block
40
+ end
41
+
42
+ def handle(message, type)
43
+ dispatch(decode(StreamEventAppeared, message).event) if
44
+ type == 'StreamEventAppeared'
45
+ end
46
+
47
+ def dispatch(event)
48
+ @position = event.original_event_number
49
+ @handler.call(event)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -3,7 +3,9 @@ module Estore
3
3
  # It also starts a background thread to read from the TCP socket and handle
4
4
  # received packages, dispatching them to the calling app.
5
5
  class Connection
6
- attr_reader :host, :port, :context, :buffer, :mutex
6
+ extend Forwardable
7
+
8
+ delegate [:register, :remove] => :@context
7
9
 
8
10
  def initialize(host, port, context)
9
11
  @host = host
@@ -18,65 +20,25 @@ module Estore
18
20
  socket.close
19
21
  end
20
22
 
21
- def send_command(command, msg = nil, handler = nil, uuid = nil)
22
- code = COMMANDS.fetch(command)
23
+ def write(uuid, command, msg = nil)
23
24
  msg.validate! if msg
24
25
 
25
- correlation_id = uuid || SecureRandom.uuid
26
- frame = Package.encode(code, correlation_id, msg)
26
+ code = COMMANDS.fetch(command)
27
+ frame = Package.encode(code, uuid, msg)
27
28
 
28
- mutex.synchronize do
29
- promise = context.register_command(correlation_id, command, handler)
29
+ @mutex.synchronize do
30
30
  socket.write(frame.to_s)
31
- promise
32
31
  end
33
32
  end
34
33
 
35
34
  private
36
35
 
37
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
38
- # rubocop:disable Metrics/MethodLength
39
- def on_received_package(command, message, uuid, _flags)
40
- case command
41
- when 'Pong'
42
- context.fulfill(uuid, 'Pong')
43
- when 'HeartbeatRequestCommand'
44
- send_command('HeartbeatResponseCommand')
45
- when 'SubscriptionConfirmation'
46
- context.fulfill(uuid, decode(SubscriptionConfirmation, message))
47
- when 'ReadStreamEventsForwardCompleted'
48
- context.fulfill(uuid, decode(ReadStreamEventsCompleted, message))
49
- when 'StreamEventAppeared'
50
- resolved_event = decode(StreamEventAppeared, message).event
51
- context.trigger(uuid, :event_appeared, resolved_event)
52
- when 'WriteEventsCompleted'
53
- on_write_events_completed(uuid, decode(WriteEventsCompleted, message))
36
+ def on_received_package(message, type, uuid, _flags)
37
+ if type == 'HeartbeatRequestCommand'
38
+ write(SecureRandom.uuid, 'HeartbeatResponseCommand')
54
39
  else
55
- raise command
56
- end
57
- end
58
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize
59
- # rubocop:enable Metrics/MethodLength
60
-
61
- def on_write_events_completed(uuid, response)
62
- if response.result != OperationResult::Success
63
- p fn: 'on_write_events_completed', at: error, result: response.result
64
- context.rejected_command(uuid, response)
65
- return
40
+ @context.dispatch(uuid, message, type)
66
41
  end
67
-
68
- context.fulfill(uuid, response)
69
- end
70
-
71
- def decode(type, message)
72
- type.decode(message)
73
- rescue => error
74
- puts "Protobuf decoding error on connection #{object_id}"
75
- puts error.inspect
76
- p type: type, message: message
77
- puts "\n\n"
78
- puts(*error.backtrace)
79
- raise error
80
42
  end
81
43
 
82
44
  def socket
@@ -84,7 +46,7 @@ module Estore
84
46
  end
85
47
 
86
48
  def connect
87
- @socket = TCPSocket.open(host, port)
49
+ @socket = TCPSocket.open(@host, @port)
88
50
  Thread.new do
89
51
  process_downstream
90
52
  end
@@ -92,12 +54,12 @@ module Estore
92
54
  rescue TimeoutError, Errno::ECONNREFUSED, Errno::EHOSTDOWN,
93
55
  Errno::EHOSTUNREACH, Errno::ENETUNREACH, Errno::ETIMEDOUT
94
56
  raise CannotConnectError, "Error connecting to Eventstore on "\
95
- "#{host.inspect}:#{port.inspect} (#{$ERROR_INFO.class})"
57
+ "#{@host.inspect}:#{@port.inspect} (#{$ERROR_INFO.class})"
96
58
  end
97
59
 
98
60
  def process_downstream
99
61
  loop do
100
- buffer << socket.sysread(4096)
62
+ @buffer << socket.sysread(4096)
101
63
  end
102
64
  rescue IOError, EOFError
103
65
  on_disconnect
@@ -112,8 +74,10 @@ module Estore
112
74
  end
113
75
 
114
76
  def on_exception(error)
115
- puts "process_downstream_error #{error.inspect}"
116
- context.on_error(error)
77
+ puts "process_downstream_error"
78
+ puts error.message
79
+ puts error.backtrace
80
+ @context.on_error(error)
117
81
  end
118
82
  end
119
83
  end
@@ -4,7 +4,6 @@ module Estore
4
4
  # packages.
5
5
  # Parsed packages are given back to the given handler as they are decoded.
6
6
  class Buffer
7
- attr_reader :buffer, :handler, :mutex
8
7
  def initialize(&block)
9
8
  @mutex = Mutex.new
10
9
  @buffer = ''.force_encoding('BINARY')
@@ -15,46 +14,37 @@ module Estore
15
14
  bytes = bytes.force_encoding('BINARY') if
16
15
  bytes.respond_to? :force_encoding
17
16
 
18
- mutex.synchronize do
17
+ @mutex.synchronize do
19
18
  @buffer << bytes
20
19
  end
21
20
 
22
- consume_available_packages
21
+ consume_packages
23
22
  end
24
23
 
25
- def consume_available_packages
26
- while consume_package
27
- end
28
- end
29
-
30
- def consume_package
31
- pkg = read_package
32
- if pkg
24
+ def consume_packages
25
+ while (pkg = read_package)
33
26
  handle(pkg)
34
27
  discard_bytes(pkg)
35
- true
36
- else
37
- false
38
28
  end
39
29
  end
40
30
 
41
31
  def read_package
42
- return nil if buffer.length < 4
43
- package_length = buffer[0...4].unpack('l<').first
44
- bytes = buffer[4...(4 + package_length)].dup
32
+ return nil if @buffer.length < 4
33
+ package_length = @buffer[0...4].unpack('l<').first
34
+ bytes = @buffer[4...(4 + package_length)].dup
45
35
  bytes if bytes.bytesize >= package_length
46
36
  end
47
37
 
48
38
  def discard_bytes(pkg)
49
- mutex.synchronize do
50
- @buffer = buffer[(4 + pkg.bytesize)..-1]
39
+ @mutex.synchronize do
40
+ @buffer = @buffer[(4 + pkg.bytesize)..-1]
51
41
  end
52
42
  end
53
43
 
54
44
  def handle(pkg)
55
45
  code, flags, uuid_bytes, message = parse(pkg)
56
46
  command = Estore::Connection.command_name(code)
57
- handler.call(command, message, Package.parse_uuid(uuid_bytes), flags)
47
+ @handler.call(message, command, Package.parse_uuid(uuid_bytes), flags)
58
48
  end
59
49
 
60
50
  def parse(pkg)
@@ -1,92 +1,28 @@
1
1
  require 'promise'
2
2
 
3
3
  module Estore
4
- # Extension of a Ruby implementation of the Promises/A+ spec
5
- # that carries the correlation id of the command.
6
- # @see https://github.com/lgierth/promise.rb
7
- class Promise < ::Promise
8
- attr_reader :correlation_id
9
-
10
- def initialize(correlation_id)
11
- super()
12
- @correlation_id = correlation_id
13
- end
14
-
15
- def wait
16
- t = Thread.current
17
- resume = proc { t.wakeup }
18
- self.then(resume, resume)
19
- sleep
20
- end
21
- end
22
-
23
- # Registry storing handlers for the outstanding commands and
24
- # current subscriptions
4
+ # Registry storing handlers for the pending commands
25
5
  class ConnectionContext
26
- attr_reader :mutex, :requests, :targets
27
6
  def initialize
28
7
  @mutex = Mutex.new
29
- @requests = {}
30
- @targets = {}
31
- end
32
-
33
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
34
- def register_command(uuid, command, target = nil)
35
- case command
36
- when 'Ping' then promise(uuid)
37
- when 'ReadStreamEventsForward' then promise(uuid)
38
- when 'SubscribeToStream' then promise(uuid, target)
39
- when 'WriteEvents' then promise(uuid)
40
- when 'HeartbeatResponseCommand' then :nothing_to_do
41
- when 'UnsubscribeFromStream' then :nothing_to_do
42
- else raise "Unknown command #{command}"
43
- end
44
- end
45
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
46
-
47
- def fulfill(uuid, value)
48
- prom = nil
49
-
50
- mutex.synchronize do
51
- prom = requests.delete(uuid)
52
- end
53
-
54
- prom.fulfill(value) if prom
8
+ @commands = {}
55
9
  end
56
10
 
57
- def rejected_command(uuid, error)
58
- prom = nil
59
-
60
- mutex.synchronize do
61
- prom = requests.delete(uuid)
11
+ def register(command)
12
+ @mutex.synchronize do
13
+ @commands[command.uuid] = command
62
14
  end
63
-
64
- prom.reject(error) if prom
65
- end
66
-
67
- def trigger(uuid, method, *args)
68
- target = mutex.synchronize { targets[uuid] }
69
- return if target.nil?
70
- target.__send__(method, *args)
71
15
  end
72
16
 
73
- def on_error(error = nil, &block)
74
- if block
75
- @error_handler = block
76
- else
77
- @error_handler.call(error) if @error_handler
17
+ def remove(command)
18
+ @mutex.synchronize do
19
+ @commands.delete(command.uuid)
78
20
  end
79
21
  end
80
22
 
81
- private
82
-
83
- def promise(uuid, target = nil)
84
- prom = Promise.new(uuid)
85
- mutex.synchronize do
86
- requests[uuid] = prom
87
- targets[uuid] = target
88
- end
89
- prom
23
+ def dispatch(uuid, message, type)
24
+ command = @commands[uuid]
25
+ command.handle(message, type) if command
90
26
  end
91
27
  end
92
28
  end
@@ -1,4 +1,4 @@
1
1
  module Estore
2
- class CannotConnectError < RuntimeError; end
3
- class DisconnectionError < RuntimeError; end
2
+ CannotConnectError = Class.new(RuntimeError)
3
+ DisconnectionError = Class.new(RuntimeError)
4
4
  end
@@ -31,70 +31,44 @@ module Estore
31
31
  end
32
32
 
33
33
  def ping
34
- command('Ping')
34
+ command(Commands::Ping)
35
35
  end
36
36
 
37
- def read(stream, start, limit)
38
- msg = ReadStreamEvents.new(
39
- event_stream_id: stream,
40
- from_event_number: start,
41
- max_count: limit,
42
- resolve_link_tos: true,
43
- require_master: false
44
- )
37
+ def read(stream, options = {})
38
+ from = options[:from] || 0
39
+ limit = options[:limit]
45
40
 
46
- command('ReadStreamEventsForward', msg)
41
+ if limit
42
+ read_batch(stream, from, limit)
43
+ else
44
+ read_forward(stream, from)
45
+ end
47
46
  end
48
47
 
49
- def append(stream, events, options = {})
50
- msg = WriteEvents.new(
51
- event_stream_id: stream,
52
- expected_version: options[:expected_version] || -2,
53
- events: Array(events).map { |event| new_event(event) },
54
- require_master: true
55
- )
56
-
57
- command('WriteEvents', msg)
48
+ def read_batch(stream, from, limit)
49
+ command(Commands::ReadBatch, stream, from, limit).call
58
50
  end
59
51
 
60
- def subscribe(stream, handler, options = {})
61
- msg = SubscribeToStream.new(
62
- event_stream_id: stream,
63
- resolve_link_tos: options[:resolve_link_tos]
64
- )
52
+ def read_forward(stream, from, batch_size = nil, &block)
53
+ command(Commands::ReadForward, stream, from, batch_size, &block).call
54
+ end
65
55
 
66
- command('SubscribeToStream', msg, handler)
56
+ def append(stream, events, options = {})
57
+ command(Commands::Append, stream, events, options).call
67
58
  end
68
59
 
69
60
  def subscription(stream, options = {})
70
- if options[:catch_up_from]
71
- CatchUpSubscription.new(self, stream, options[:catch_up_from], options)
61
+ if options[:from]
62
+ command(Commands::CatchUpSubscription, stream, options[:from], options)
72
63
  else
73
- Subscription.new(self, stream, options)
64
+ command(Commands::Subscription, stream, options)
74
65
  end
75
66
  end
76
67
 
77
68
  private
78
69
 
79
- CONTENT_TYPES = {
80
- json: 1
81
- }
82
-
83
- def new_event(event)
84
- uuid = event[:id] || SecureRandom.uuid
85
- content_type = event.fetch(:content_type, :json)
86
-
87
- NewEvent.new(
88
- event_id: Package.encode_uuid(uuid),
89
- event_type: event[:type],
90
- data: event[:data],
91
- data_content_type: CONTENT_TYPES.fetch(content_type, 0),
92
- metadata_content_type: 1
93
- )
94
- end
95
-
96
- def command(*args)
97
- connection.send_command(*args)
70
+ def command(command, *args)
71
+ command.new(connection, *args)
98
72
  end
99
73
  end
100
74
  end
@@ -1,3 +1,3 @@
1
1
  module Estore
2
- VERSION = '0.0.4'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -33,74 +33,88 @@ describe Estore::Session do
33
33
  "test-#{SecureRandom.uuid}"
34
34
  end
35
35
 
36
- def populate(count, stream = nil)
36
+ def stream_with(count, stream = nil)
37
37
  stream ||= random_stream
38
38
  session.append(stream, events(count)).sync
39
39
  stream
40
40
  end
41
41
 
42
- it 'reads events from a stream' do
43
- stream = populate(20)
44
- read = session.read(stream, 0, 20).sync
42
+ RSpec::Matchers.define :start_from do |start|
43
+ match do |events|
44
+ events.each_with_index do |event, index|
45
+ expect(parse_data(event)).to eql('id' => index + start)
46
+ end
47
+ end
48
+ end
45
49
 
46
- expect(read.events.size).to be(20)
50
+ it 'reads all the events from a stream' do
51
+ events = session.read(stream_with(200)).sync
47
52
 
48
- read.events.each_with_index do |event, index|
49
- expect(parse_data(event)).to eql('id' => index)
50
- end
53
+ expect(events.size).to be(200)
54
+ expect(events).to start_from(0)
55
+ end
56
+
57
+ it 'reads all the events forward from a stream' do
58
+ events = session.read(stream_with(100), from: 20).sync
59
+
60
+ expect(events.size).to be(80)
61
+ expect(events).to start_from(20)
62
+ end
63
+
64
+ it 'reads a batch of events from a stream' do
65
+ events = session.read(stream_with(30), from: 10, limit: 15).sync
66
+
67
+ expect(events.size).to be(15)
68
+ expect(events).to start_from(10)
51
69
  end
52
70
 
53
71
  it 'allows to make a live subscription' do
54
72
  stream = random_stream
55
- received = 0
73
+ received = []
56
74
 
57
- sub = session.subscription(stream)
58
- sub.on_error { |error| raise error.inspect }
59
-
60
- sub.on_event do |event|
61
- expect(parse_data(event)).to eql('id' => received)
62
- received += 1
63
- end
75
+ stream_with(20, stream)
64
76
 
77
+ sub = session.subscription(stream)
78
+ sub.on_event { |event| received << event }
65
79
  sub.start
66
80
 
67
- populate(50, stream)
81
+ stream_with(50, stream)
68
82
 
69
83
  Timeout.timeout(5) do
70
84
  loop do
71
- break if received >= 50
85
+ break if received.size >= 50
72
86
  sleep(0.1)
73
87
  end
74
88
  end
89
+
90
+ expect(received.size).to be(50)
91
+ expect(received).to start_from(20)
75
92
  end
76
93
 
77
94
  it 'allows to make a catchup subscription' do
78
95
  stream = random_stream
79
- received = 0
96
+ received = []
80
97
 
81
- populate(50, stream)
82
-
83
- sub = session.subscription(stream, catch_up_from: 20)
84
- sub.on_error { |error| raise error.inspect }
85
-
86
- sub.on_event do |event|
87
- expect(parse_data(event)).to eql('id' => received + 20)
88
- received += 1
89
- end
98
+ stream_with(2100, stream)
90
99
 
100
+ sub = session.subscription(stream, from: 20)
101
+ sub.on_event { |event| received << event }
91
102
  sub.start
92
103
 
93
104
  Thread.new do
94
105
  50.times do
95
- populate(2, stream)
106
+ stream_with(2, stream)
96
107
  end
97
108
  end
98
109
 
99
110
  Timeout.timeout(5) do
100
111
  loop do
101
- break if received >= 130
112
+ break if received.size >= 2180
102
113
  sleep(0.1)
103
114
  end
104
115
  end
116
+
117
+ expect(received.size).to be(2180)
118
+ expect(received).to start_from(20)
105
119
  end
106
120
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: estore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mathieu Ravaux
@@ -57,16 +57,16 @@ dependencies:
57
57
  name: rake
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - ">="
60
+ - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: '0'
62
+ version: '10.0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - ">="
67
+ - - "~>"
68
68
  - !ruby/object:Gem::Version
69
- version: '0'
69
+ version: '10.0'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: rubocop
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +81,7 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: 0.28.0
84
- description: An Event Store driver for Ruby
84
+ description: TCP driver to read and write events to Event Store
85
85
  email:
86
86
  - mathieu.ravaux@gmail.com
87
87
  - hector0193@gmail.com
@@ -100,17 +100,24 @@ files:
100
100
  - Rakefile
101
101
  - estore.gemspec
102
102
  - lib/estore.rb
103
- - lib/estore/catchup_subscription.rb
103
+ - lib/estore/commands.rb
104
+ - lib/estore/commands/append.rb
105
+ - lib/estore/commands/base.rb
106
+ - lib/estore/commands/catch_up_subscription.rb
107
+ - lib/estore/commands/ping.rb
108
+ - lib/estore/commands/promise.rb
109
+ - lib/estore/commands/read_batch.rb
110
+ - lib/estore/commands/read_forward.rb
111
+ - lib/estore/commands/subscription.rb
104
112
  - lib/estore/connection.rb
105
113
  - lib/estore/connection/buffer.rb
106
- - lib/estore/connection/commands.rb
114
+ - lib/estore/connection/protocol.rb
107
115
  - lib/estore/connection_context.rb
108
116
  - lib/estore/errors.rb
109
117
  - lib/estore/message_extensions.rb
110
118
  - lib/estore/messages.rb
111
119
  - lib/estore/package.rb
112
120
  - lib/estore/session.rb
113
- - lib/estore/subscription.rb
114
121
  - lib/estore/version.rb
115
122
  - spec/integration/session_spec.rb
116
123
  - spec/spec_helper.rb
@@ -1,87 +0,0 @@
1
- module Estore
2
- # Catch-Up Subscriptions
3
- #
4
- # This kind of subscription specifies a starting point, in the form of an
5
- # event number or transaction file position. The given function will be
6
- # called for events from the starting point until the end of the stream,
7
- # and then for subsequently written events.
8
- #
9
- # For example, if a starting point of 50 is specified when a stream has 100
10
- # events in it, the subscriber can expect to see events 51 through 100, and
11
- # then any events subsequently written until such time as the subscription is
12
- # dropped or closed.
13
- class CatchUpSubscription < Subscription
14
- MAX_READ_BATCH = 100
15
-
16
- attr_reader :from, :caught_up
17
-
18
- def initialize(estore, stream, from, options = {})
19
- super(estore, stream, options)
20
-
21
- @from = from
22
- @caught_up = false
23
- @mutex = Mutex.new
24
- @queue = []
25
- @position = from - 1
26
- @batch_size = options[:batch_size] || 100
27
- end
28
-
29
- def on_catchup(&block)
30
- @on_catchup = block if block
31
- end
32
-
33
- def start
34
- subscribe
35
- backfill
36
- switch_to_live
37
- call_on_catchup
38
- end
39
-
40
- private
41
-
42
- def event_appeared(event)
43
- unless caught_up
44
- @mutex.synchronize do
45
- @queue.push(event) unless caught_up
46
- end
47
- end
48
- dispatch(event) if caught_up
49
- end
50
-
51
- def switch_to_live
52
- @mutex.synchronize do
53
- dispatch_events(received_while_backfilling)
54
- @queue = nil
55
- @caught_up = true
56
- end
57
- end
58
-
59
- def backfill
60
- loop do
61
- events, finished = fetch_batch(@position + 1)
62
- @mutex.synchronize do
63
- dispatch_events(events)
64
- end
65
- break if finished
66
- end
67
- end
68
-
69
- def dispatch_events(events)
70
- events.each { |e| dispatch(e) }
71
- end
72
-
73
- def fetch_batch(from)
74
- prom = @estore.read(stream, from, @batch_size)
75
- response = prom.sync
76
- [Array(response.events), response.is_end_of_stream]
77
- end
78
-
79
- def received_while_backfilling
80
- @queue.find_all { |event| event.original_event_number > @position }
81
- end
82
-
83
- def call_on_catchup
84
- @on_catchup.call if @on_catchup
85
- end
86
- end
87
- end
@@ -1,57 +0,0 @@
1
- module Estore
2
- # Volatile Subscriptions
3
- #
4
- # This kind of subscription calls a given function for events written
5
- # after the subscription is established.
6
- #
7
- # For example, if a stream has 100 events in it when a subscriber connects,
8
- # the subscriber can expect to see event number 101 onwards until the time
9
- # the subscription is closed or dropped.
10
- class Subscription
11
- attr_reader :id, :stream, :resolve_link_tos, :position
12
-
13
- def initialize(estore, stream, options = {})
14
- @estore = estore
15
- @stream = stream
16
- @resolve_link_tos = options.fetch(:resolve_link_tos, true)
17
- end
18
-
19
- def on_error(&block)
20
- @on_error = block if block
21
- end
22
-
23
- def on_event(&block)
24
- @on_event = block if block
25
- end
26
-
27
- def start
28
- subscribe
29
- end
30
-
31
- def stop
32
- @estore.unsubscribe(id) if id
33
- @id = nil
34
- end
35
-
36
- private
37
-
38
- def subscribe
39
- prom = @estore.subscribe(stream, self, resolve_link_tos: resolve_link_tos)
40
- @id = prom.correlation_id
41
- prom.sync
42
- end
43
-
44
- def call_on_error(error)
45
- @on_error.call(error) if @on_error
46
- end
47
-
48
- def dispatch(event)
49
- @on_event.call(event) if @on_event
50
- @position = event.original_event_number
51
- end
52
-
53
- def event_appeared(event)
54
- dispatch(event)
55
- end
56
- end
57
- end