akasha 0.3.0 → 0.4.0.pre.edge.pre.5

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: 4078cc01d0c132a3b675ed21de42246880643c49
4
- data.tar.gz: 0562b5f9607dea81f47720c680d3f5517c5190e0
3
+ metadata.gz: 4ef5ba87a94db893bc51fa6e9473ce73007ef1cd
4
+ data.tar.gz: 8c83ff0c79d88bd3dd939c11ed7bde46ad1be4de
5
5
  SHA512:
6
- metadata.gz: 114a107b63f0f25d9e21b5a00c8a4277885ca705ff7d324199a89e54004c41f410380608d7ba994e8d2f0e3dab220c8a09443c56ef36bc69c551f061d23b3366
7
- data.tar.gz: 4ef66f8424b38a10ac6b8b4095e9a42f985babf00966d5e7d8928cc8fd588768cf1ee45baa516fe094a8b717ee560d44e5f5856028aa6af97eee806b88c3c1ca
6
+ metadata.gz: 4a787f8c35533063aeacb5071e1dfa6db0b79a892816a3c98d83eb99da9954f0b2c2138df325d1a0c0c37aed53cefed7fb0ba39c71ae38a8e000b89bba1a03db
7
+ data.tar.gz: 31e9a1d1fa1abc7833a4302955554e5b4b34f3555f16118fca09b450fb187abe85dd845ced94ec361b634ddaca1ff955ab1997d1c0e3e2da2e9624b14dca1595
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
1
  /pkg
2
+ /.bundle
3
+ /vendor
data/.rubocop.yml CHANGED
@@ -5,6 +5,7 @@ Metrics/AbcSize:
5
5
  Metrics/BlockLength:
6
6
  Exclude:
7
7
  - spec/**/*_spec.rb
8
+ - spec/**/*_shared_examples.rb
8
9
  - akasha.gemspec
9
10
  Metrics/ClassLength:
10
11
  Max: 110
@@ -14,3 +15,5 @@ Metrics/MethodLength:
14
15
  Max: 15
15
16
  Rails/Output:
16
17
  Enabled: false
18
+ Style/FormatStringToken:
19
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,41 +1,36 @@
1
- # Enables Travis to use their new container-based infrastructure
2
1
  sudo: false
3
2
 
4
- # Integration tests are using docker-compose
5
3
  services:
6
- - docker
4
+ - docker
7
5
 
8
- # Build for Ruby
9
6
  language: ruby
10
7
 
11
- # Enables caching for bundler
12
8
  cache: bundler
13
9
 
14
- # Passes arguments to bundle install (http://gembundler.com/man/bundle-install.1.html)
15
- # bundler_args:
16
-
17
10
  before_install:
18
- - gem update --system
19
- - gem update bundler
11
+ - gem update --system
12
+ - gem update bundler
13
+ - bundle update
20
14
 
21
- # Specify which ruby versions you wish to run your tests on, each version will be used
22
15
  matrix:
23
16
  include:
24
- - rvm: 2.3.6
25
- - rvm: 2.5.1
26
- - env: INTEGRATION_TESTS=true
17
+ - rvm: 2.3.6
18
+ # - rvm: 2.5.1
19
+ # - env: INTEGRATION_TESTS=true
27
20
 
28
- # Define how to run your tests.
29
21
  script:
30
- - |
31
- # Run either integration tests or specs.
32
- case "$INTEGRATION_TESTS" in
33
- true) bin/integration-tests.sh ;;
34
- *) bundle exec rspec --tag ~integration ;;
35
- esac
36
- # Check if conventions are being followed.
37
- - bundle exec rake rubocop
38
-
39
- # Disable email notifications
22
+ - |
23
+ # Run either integration tests or specs.
24
+ case "$INTEGRATION_TESTS" in
25
+ true) bin/integration-tests.sh ;;
26
+ *) bundle exec rspec --tag ~integration ;;
27
+ esac
28
+ - bundle exec rake rubocop
29
+
40
30
  notifications:
41
- disabled: true
31
+ disabled: true
32
+
33
+ deploy:
34
+ provider: rubygems
35
+ api_key:
36
+ secure: EGspR+X6oNBQMMZO3QRXT6RW5tglTj3X9/9HiPJQDCnAnBCWxrxBdvrlhf4sMSrcfuxXt7mGeXaoD/sOrHS7BOmVE6SrJJhJgph8Uv3EnfEE4V8LswjGXRfzf+6g6PxGAR2pcaYdGfyQVb0wLvAFArjs7aBg+8+fisP+ffwi6ahPd2YzUNb0Oh4J0rvmqGt6KCXR9z/4OKzNyweHmXLM+PfZr5uRs8NT70tx+n8zo5YrgCk5t8odQTchsKWGkFF8dzP3mzjQTt3d0MW0rkykgjd+rFCVmLA1L4aErkLAQxNM8YogsrS7hptwTF390vhL0yTyRQpY8fQU+U8igwtP3XcoXw5cngdZDR3vFUo7RgxJiIVESPRGJ8uTkxMzX7rkyDNAcLj82cT2etouKgAxK9G5/nvC6IEVF6x3AkH93gruNZEHj2BRXKYIhL3o6u6CeUuA9KM3jQI5FeAKzQ19yf8wk+wu/I3pK83evMtSQ07cDHOgzdHjZjoUOBZlshrKSkkByBS+ueJmZRg2flTtGkxpy0hzgHjQ12HFOPYrTKEICMgGKhAohUQoUji/+iHAzaRZygfqJKLXJKPAR0JGumvEqCuZfcvL+T+Cg4R2RZLbdvF9YN42aVvSJNTS6/lA5J0+1cm+9XCZFYX0NR9CD85wfzU9zBHh0obYt61zgvI=
data/CHANGELOG.md CHANGED
@@ -1,22 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## Version 0.4.0.edge3
4
+
5
+ * Fixed issue for passsing Handlers to EventRouter via constructor, when they aren't wrapped in array.
6
+
7
+ ## Version 0.4.0.edge2
8
+
9
+ * Control the maximum number of retries in case of network related failures. [#14](https://github.com/bilus/akasha/pull/14)
10
+ Example:
11
+
12
+ ```ruby
13
+ store = Akasha::Storage::HttpEventStore.new(..., max_retries: 10)
14
+ ```
15
+
16
+
17
+ ## Version 0.4.0.edge1
18
+
19
+ * Optional namespacing for aggregate/projection streams and events allowing for isolation
20
+ between applications. [#12](https://github.com/bilus/akasha/pull/12)
21
+ * Fix Unhandled events in stream break aggregate loading. [Issue #5](https://github.com/bilus/akasha/issues/5)
22
+
23
+
3
24
  ## Version 0.3.0
4
25
 
5
- * Asynchronous event listeners (`AsyncEventRouter`).
6
- * Simplified initialization of event- and command routers.
26
+ * Asynchronous event listeners (`AsyncEventRouter`). [#9](https://github.com/bilus/akasha/pull/9)
27
+ * Simplified initialization of event- and command routers. [#10](https://github.com/bilus/akasha/pull/10)
7
28
  * Remove dependency on the `http_event_store` gem.
8
- * `Event#metadata` is no longer OpenStruct.
29
+ * `Event#metadata` is no longer OpenStruct. [#10](https://github.com/bilus/akasha/pull/10)
9
30
 
10
31
  ## Version 0.2.0
11
32
 
12
- * Synchronous event listeners (see `examples/sinatra/app.rb`).
13
- * HTTP-based Eventstore storage.
33
+ * Synchronous event listeners (see `examples/sinatra/app.rb`). [#4](https://github.com/bilus/akasha/pull/4)
34
+ * HTTP-based Eventstore storage. [#7](https://github.com/bilus/akasha/pull/7)
14
35
 
15
36
 
16
37
  ## Version 0.1.0
17
38
 
18
- * Cleaner syntax for adding events to changesets: `changeset.append(:it_happened, foo: 'bar')`.
19
- * Support for command routing (`Akasha::CommandRouter`).
39
+ * Cleaner syntax for adding events to changesets: `changeset.append(:it_happened, foo: 'bar')`. [#1](https://github.com/bilus/akasha/pull/1)
40
+ * Support for command routing (`Akasha::CommandRouter`). [#2](https://github.com/bilus/akasha/pull/2)
20
41
 
21
42
 
22
43
  ## Version 0.0.1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- akasha (0.3.0)
4
+ akasha (0.4.0.pre.edge.pre.5)
5
5
  corefines (~> 1.11)
6
6
  faraday (~> 0.15)
7
7
  faraday_middleware
@@ -12,7 +12,7 @@ PATH
12
12
  GEM
13
13
  remote: https://rubygems.org/
14
14
  specs:
15
- ast (2.3.0)
15
+ ast (2.4.0)
16
16
  byebug (10.0.2)
17
17
  coderay (1.1.2)
18
18
  corefines (1.11.0)
@@ -24,12 +24,13 @@ GEM
24
24
  faraday_middleware (0.12.2)
25
25
  faraday (>= 0.7.4, < 1.0)
26
26
  ffi (1.9.25)
27
+ jaro_winkler (1.5.1)
27
28
  method_source (0.9.0)
28
29
  multipart-post (2.0.0)
29
- parallel (1.12.0)
30
- parser (2.4.0.0)
31
- ast (~> 2.2)
32
- powerpack (0.1.1)
30
+ parallel (1.12.1)
31
+ parser (2.5.1.0)
32
+ ast (~> 2.4.0)
33
+ powerpack (0.1.2)
33
34
  pry (0.11.3)
34
35
  coderay (~> 1.1.0)
35
36
  method_source (~> 0.9.0)
@@ -37,8 +38,7 @@ GEM
37
38
  byebug (~> 10.0)
38
39
  pry (~> 0.10)
39
40
  rack (2.0.5)
40
- rainbow (2.2.2)
41
- rake
41
+ rainbow (3.0.0)
42
42
  rake (10.5.0)
43
43
  retries (0.0.5)
44
44
  rspec (3.7.0)
@@ -56,18 +56,19 @@ GEM
56
56
  rspec-support (3.7.1)
57
57
  rspec-wait (0.0.9)
58
58
  rspec (>= 3, < 4)
59
- rubocop (0.50.0)
59
+ rubocop (0.57.2)
60
+ jaro_winkler (~> 1.5.1)
60
61
  parallel (~> 1.10)
61
- parser (>= 2.3.3.1, < 3.0)
62
+ parser (>= 2.5)
62
63
  powerpack (~> 0.1)
63
- rainbow (>= 2.2.2, < 3.0)
64
+ rainbow (>= 2.2.2, < 4.0)
64
65
  ruby-progressbar (~> 1.7)
65
66
  unicode-display_width (~> 1.0, >= 1.0.1)
66
67
  ruby-progressbar (1.9.0)
67
68
  timecop (0.9.1)
68
69
  typhoeus (1.3.0)
69
70
  ethon (>= 0.9.0)
70
- unicode-display_width (1.3.0)
71
+ unicode-display_width (1.4.0)
71
72
 
72
73
  PLATFORMS
73
74
  ruby
data/README.md CHANGED
@@ -30,23 +30,23 @@ This library itself makes no assumptions about any web framework, you can use it
30
30
  - [x] HTTP Eventstore storage backend
31
31
  - [x] Event#id for better idempotence (validate this claim)
32
32
  - [x] Async EventHandlers (storing cursors in Eventstore, configurable durability guarantees)
33
- - [x] Uniform intetrface for Client -- use Event.
33
+ - [x] Uniform interface for Client -- use Event.
34
34
  - [x] Rewrite Client
35
35
  - [x] Refactor Client code
36
36
  - [x] Take care of created_at/updated_at (saved_at?)
37
37
  - [x] Tests for HttpEventStore
38
38
  - [x] Projections
39
39
  - [x] Test for AsyncEventRouter using events not aggregate
40
- - [x] BUG: Projection reorders events
40
+ - [x] BUG: Projection reorders events (need to use fromAll after all)
41
41
  - [x] Simplify AsyncEventRouter init
42
42
  - [x] SyncEventRouter => EventRouter
43
43
  - [x] Metadata not persisted
44
44
  - [x] Refactoring & simplification.
45
45
  - [x] Hash-based event and command router
46
- - [x] Do we need EventListener class? Yes.
47
- - [x] Assymetry between data and metadata.
48
- - [x] Faster shutdown.
49
- - [ ] Namespacing for events and aggregates and the projection
46
+ - [x] Assymetry between data and metadata
47
+ - [x] Faster shutdown
48
+ - [x] Namespacing for events and aggregates and the projection
49
+ - [x] Way to control the number of retries in face of network failures
50
50
  - [ ] Version-based concurrency
51
51
  - [ ] Telemetry (Dogstatsd)
52
52
  - [ ] Socket-based Eventstore storage backend
data/Rakefile CHANGED
@@ -2,6 +2,22 @@ require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
3
  require 'rubocop/rake_task'
4
4
 
5
+ namespace 'ci' do
6
+ task 'tag' do
7
+ gemspecs = Dir[File.join(__dir__, '{,*}.gemspec')]
8
+ raise 'No gemspec found' unless gemspecs.size == 1
9
+ spec_path = gemspecs.first
10
+ gemspec = Bundler.load_gemspec(spec_path)
11
+ version_tag = "v#{gemspec.version}"
12
+ return if `git tag`.split(/\n/).include?(version_tag)
13
+ `git tag #{version_tag}`
14
+ `git push --tags`
15
+ end
16
+
17
+ task 'release' => ['build', 'release:guard_clean', 'release:rubygem_push', 'ci:tag'] do
18
+ end
19
+ end
20
+
5
21
  RuboCop::RakeTask.new(:rubocop) do |task|
6
22
  task.options = ['--rails', '--display-cop-names']
7
23
  end
data/akasha.gemspec CHANGED
@@ -1,11 +1,15 @@
1
-
1
+ # rubocop:disable Style/ExpandPathArguments
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'akasha/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'akasha'
8
+ # rubocop:disable Gemspec/DuplicatedAssignment
8
9
  spec.version = Akasha::VERSION
10
+ spec.version = "#{spec.version}-#{ENV['TRAVIS_BUILD_NUMBER']}" \
11
+ if ENV['TRAVIS'] && spec.version.include('edge')
12
+ # rubocop:enable Gemspec/DuplicatedAssignment
9
13
  spec.authors = ['Marcin Bilski']
10
14
  spec.email = ['marcin@tooploox.com']
11
15
 
@@ -23,12 +27,12 @@ Gem::Specification.new do |spec|
23
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
28
  spec.require_paths = ['lib']
25
29
 
30
+ spec.add_dependency 'corefines', '~>1.11'
26
31
  spec.add_dependency 'faraday', '~> 0.15'
27
32
  spec.add_dependency 'faraday_middleware'
28
- spec.add_dependency 'typhoeus', '~> 1.3'
29
33
  spec.add_dependency 'rack', '~> 2.0'
30
34
  spec.add_dependency 'retries', '~> 0.0'
31
- spec.add_dependency 'corefines', '~>1.11'
35
+ spec.add_dependency 'typhoeus', '~> 1.3'
32
36
 
33
37
  spec.add_development_dependency 'bundler', '~> 1.16'
34
38
  spec.add_development_dependency 'pry-byebug'
@@ -38,3 +42,4 @@ Gem::Specification.new do |spec|
38
42
  spec.add_development_dependency 'rubocop', '~> 0.50'
39
43
  spec.add_development_dependency 'timecop', '~> 0.9'
40
44
  end
45
+ # rubocop:enable Style/ExpandPathArguments
@@ -57,7 +57,7 @@ class MyAkashaApp
57
57
  username: 'admin',
58
58
  password: 'changeit'
59
59
  ),
60
- namespace: :my_app
60
+ namespace: :'sinatra.example.akasha.bilus.io'
61
61
  )
62
62
  Akasha::Aggregate.connect!(repository)
63
63
 
@@ -29,7 +29,8 @@ module Akasha
29
29
  # Used by Repository.
30
30
  def apply_events(events)
31
31
  events.each do |event|
32
- send(event_handler(event), event.data)
32
+ method_name = event_handler(event)
33
+ send(method_name, event.data) if respond_to?(method_name)
33
34
  end
34
35
  end
35
36
 
@@ -9,13 +9,16 @@ module Akasha
9
9
  DEFAULT_PAGE_SIZE = 20
10
10
  DEFAULT_PROJECTION_STREAM = 'AsyncEventRouter'.freeze
11
11
  DEFAULT_CHECKPOINT_STRATEGY = Akasha::Checkpoint::HttpEventStoreCheckpoint
12
+ STREAM_NAME_SEP = '-'.freeze
12
13
 
13
- def connect!(repository, projection_name: DEFAULT_PROJECTION_STREAM,
14
+ def connect!(repository, projection_name: nil,
14
15
  checkpoint_strategy: DEFAULT_CHECKPOINT_STRATEGY,
15
16
  page_size: DEFAULT_PAGE_SIZE, poll: DEFAULT_POLL_SECONDS)
17
+ projection_name = projection_name(repository) if projection_name.nil?
16
18
  projection_stream = repository.store.streams[projection_name]
17
19
  checkpoint = checkpoint_strategy.is_a?(Class) ? checkpoint_strategy.new(projection_stream) : checkpoint_strategy
18
- repository.merge_all_by_event(into: projection_name, only: registered_event_names)
20
+ repository.merge_all_by_event(into: projection_name,
21
+ only: registered_event_names)
19
22
  Thread.new do
20
23
  run_forever(projection_stream, checkpoint, page_size, poll)
21
24
  end
@@ -40,5 +43,12 @@ module Akasha
40
43
  end
41
44
  end
42
45
  end
46
+
47
+ def projection_name(repository)
48
+ parts = []
49
+ parts << repository.namespace unless repository.namespace.nil?
50
+ parts << DEFAULT_PROJECTION_STREAM
51
+ parts.join(STREAM_NAME_SEP)
52
+ end
43
53
  end
44
54
  end
@@ -14,10 +14,12 @@ module Akasha
14
14
  raise UnsupportedStorageError, "Storage does not support checkpoints: #{stream.class}"
15
15
  end
16
16
 
17
+ # rubocop:disable Naming/MemoizedInstanceVariableName
17
18
  # Returns the most recently stored next position.
18
19
  def latest
19
20
  @next_position ||= (read_position || 0)
20
21
  end
22
+ # rubocop:enable Naming/MemoizedInstanceVariableName
21
23
 
22
24
  # Returns the next position, conditionally storing it (based on the configurable interval).
23
25
  def ack(position)
data/lib/akasha/event.rb CHANGED
@@ -20,5 +20,9 @@ module Akasha
20
20
  data == other.data &&
21
21
  metadata == other.metadata
22
22
  end
23
+
24
+ def with_metadata(metadata)
25
+ Event.new(@name, @id, @metadata.merge(metadata), **@data)
26
+ end
23
27
  end
24
28
  end
@@ -3,7 +3,9 @@ module Akasha
3
3
  class EventRouterBase
4
4
  def initialize(routes = {})
5
5
  @routes = Hash.new { |hash, key| hash[key] = [] }
6
- @routes.merge!(routes)
6
+ # Support both array of listeners and a single listener
7
+ normalized_routes = routes.map { |command, listeners| [command, Array(listeners)] }.to_h
8
+ @routes.merge!(normalized_routes)
7
9
  end
8
10
 
9
11
  # Registers a new event listener, derived from
@@ -3,14 +3,15 @@ module Akasha
3
3
  # Not meant to be used directly (see aggregate/syntax_helpers.rb)
4
4
  # See specs for usage.
5
5
  class Repository
6
- attr_reader :store
6
+ attr_reader :store, :namespace
7
7
 
8
8
  STREAM_NAME_SEP = '-'.freeze
9
9
 
10
10
  # Creates a new repository using the underlying `store` (e.g. `MemoryEventStore`).
11
- def initialize(store)
11
+ def initialize(store, namespace: nil)
12
12
  @store = store
13
13
  @subscribers = []
14
+ @namespace = namespace
14
15
  end
15
16
 
16
17
  # Loads an aggregate identified by `id` and `klass` from the repository.
@@ -31,8 +32,9 @@ module Akasha
31
32
  # Saves an aggregate to the repository, appending events to the corresponding stream.
32
33
  def save_aggregate(aggregate)
33
34
  changeset = aggregate.changeset
34
- stream(aggregate.class, changeset.aggregate_id).write_events(changeset.events)
35
- notify_subscribers(aggregate)
35
+ events = changeset.events.map { |event| event.with_metadata(namespace: @namespace) }
36
+ stream(aggregate.class, changeset.aggregate_id).write_events(events)
37
+ notify_subscribers(changeset.aggregate_id, events)
36
38
  end
37
39
 
38
40
  # Subscribes to event streams passing either a lambda or a block.
@@ -54,25 +56,27 @@ module Akasha
54
56
  # `into` - name of the new stream
55
57
  # `only` - array of event names
56
58
  def merge_all_by_event(into:, only:)
57
- @store.merge_all_by_event(into: into, only: only)
59
+ @store.merge_all_by_event(into: into, only: only, namespace: @namespace)
58
60
  end
59
61
 
60
62
  private
61
63
 
62
64
  def stream_name(aggregate_klass, aggregate_id)
63
- "#{aggregate_klass}#{STREAM_NAME_SEP}#{aggregate_id}"
65
+ parts = []
66
+ parts << @namespace if @namespace
67
+ parts << aggregate_klass
68
+ parts << aggregate_id
69
+ parts.join(STREAM_NAME_SEP)
64
70
  end
65
71
 
66
72
  def stream(aggregate_klass, aggregate_id)
67
73
  @store.streams[stream_name(aggregate_klass, aggregate_id)]
68
74
  end
69
75
 
70
- def notify_subscribers(aggregate)
71
- id = aggregate.changeset.aggregate_id
72
- events = aggregate.changeset.events
76
+ def notify_subscribers(aggregate_id, events)
73
77
  @subscribers.each do |subscriber|
74
78
  events.each do |event|
75
- subscriber.call(id, event)
79
+ subscriber.call(aggregate_id, event)
76
80
  end
77
81
  end
78
82
  end
@@ -1,34 +1,17 @@
1
1
  require_relative 'http_event_store/client'
2
2
  require_relative 'http_event_store/stream'
3
+ require_relative 'http_event_store/exceptions'
3
4
 
4
5
  module Akasha
5
6
  module Storage
6
7
  # HTTP-based interface to Eventstore (https://geteventstore.com)
7
8
  class HttpEventStore
8
- # Base class for all HTTP Event store errors.
9
- Error = Class.new(RuntimeError)
10
- # Stream name contains invalid characters.
11
- InvalidStreamNameError = Class.new(Error)
12
-
13
- # Base class for HTTP errors.
14
- class HttpError < Error
15
- attr_reader :status_code
16
-
17
- def initialize(status_code)
18
- @status_code = status_code
19
- super("Unexpected HTTP response: #{@status_code}")
20
- end
21
- end
22
-
23
- # 4xx HTTP status code.
24
- HttpClientError = Class.new(HttpError)
25
- # 5xx HTTP status code.
26
- HttpServerError = Class.new(HttpError)
27
-
28
- # Creates a new event store client, connecting to the specified host and port
29
- # using an optional username and password.
30
- def initialize(host: 'localhost', port: 2113, username: nil, password: nil)
9
+ # Creates a new event store client, connecting to the specified `host` and `port`
10
+ # using an optional `username` and `password`.
11
+ # Use the `max_retries` option to choose how many times to retry in case of network failures.
12
+ def initialize(host: 'localhost', port: 2113, username: nil, password: nil, max_retries: 0)
31
13
  @client = Client.new(host: host, port: port, username: username, password: password)
14
+ @max_retries = max_retries
32
15
  end
33
16
 
34
17
  # Returns a Hash of streams. You can retrieve a Stream instance corresponding
@@ -40,7 +23,7 @@ module Akasha
40
23
 
41
24
  # Shortcut for accessing streams by their names.
42
25
  def [](stream_name)
43
- Stream.new(@client, stream_name)
26
+ Stream.new(@client, stream_name, max_retries: @max_retries)
44
27
  end
45
28
 
46
29
  # Merges all streams into one, filtering the resulting stream
@@ -50,8 +33,10 @@ module Akasha
50
33
  # Arguments:
51
34
  # `into` - name of the new stream
52
35
  # `only` - array of event names
53
- def merge_all_by_event(into:, only:)
54
- @client.merge_all_by_event(into, only)
36
+ # `namespace` - optional namespace; if provided, the resulting stream will
37
+ # only contain events with the same `metadata[:namespace]`
38
+ def merge_all_by_event(into:, only:, namespace: nil)
39
+ @client.merge_all_by_event(into, only, namespace: namespace, max_retries: @max_retries)
55
40
  end
56
41
  end
57
42
  end
@@ -56,9 +56,12 @@ module Akasha
56
56
  # Arguments:
57
57
  # `name` - name of the projection stream
58
58
  # `event_names` - array of event names
59
- def merge_all_by_event(name, event_names, max_retries: 0)
59
+ # `namespace` - optional namespace; if provided, the resulting stream will
60
+ # only contain events with the same metadata.namespace
61
+ # `max_retries` - how many times to retry in case of network failures
62
+ def merge_all_by_event(name, event_names, namespace: nil, max_retries: 0)
60
63
  retrying_on_network_failures(max_retries) do
61
- ProjectionManager.new(self).merge_all_by_event(name, event_names)
64
+ ProjectionManager.new(self).merge_all_by_event(name, event_names, namespace: namespace)
62
65
  end
63
66
  end
64
67
 
@@ -70,9 +73,9 @@ module Akasha
70
73
  end
71
74
 
72
75
  # Updates stream metadata.
73
- def retry_write_metadata(stream_name, metadata)
76
+ def retry_write_metadata(stream_name, metadata, max_retries: 0)
74
77
  event = Akasha::Event.new(:stream_metadata_changed, SecureRandom.uuid, metadata)
75
- retry_append_to_stream("#{stream_name}/metadata", [event])
78
+ retry_append_to_stream("#{stream_name}/metadata", [event], max_retries: max_retries)
76
79
  end
77
80
 
78
81
  # Issues a generic request against the API.
@@ -0,0 +1,27 @@
1
+ module Akasha
2
+ module Storage
3
+ class HttpEventStore
4
+ # Base class for all HTTP Event store errors.
5
+ Error = Class.new(RuntimeError)
6
+
7
+ # Stream name contains invalid characters.
8
+ InvalidStreamNameError = Class.new(Error)
9
+
10
+ # Base class for HTTP errors.
11
+ class HttpError < Error
12
+ attr_reader :status_code
13
+
14
+ def initialize(status_code)
15
+ @status_code = status_code
16
+ super("Unexpected HTTP response: #{@status_code}")
17
+ end
18
+ end
19
+
20
+ # 4xx HTTP status code.
21
+ HttpClientError = Class.new(HttpError)
22
+
23
+ # 5xx HTTP status code.
24
+ HttpServerError = Class.new(HttpError)
25
+ end
26
+ end
27
+ end
@@ -14,32 +14,54 @@ module Akasha
14
14
  # Arguments:
15
15
  # `name` - name of the projection stream
16
16
  # `event_names` - array of event names
17
- def merge_all_by_event(name, event_names)
18
- attempt_create_projection(name, event_names) ||
19
- update_projection(name, event_names)
17
+ # `namespace` - optional namespace; if provided, the resulting stream will
18
+ # only contain events with the same metadata.namespace
19
+ def merge_all_by_event(name, event_names, namespace: nil)
20
+ attempt_create_projection(name, event_names, namespace) ||
21
+ update_projection(name, event_names, namespace)
20
22
  end
21
23
 
22
24
  private
23
25
 
24
- def projection_javascript(name, events)
25
- callbacks = events.map { |en| "\"#{en}\": function(s,e) { linkTo('#{name}', e) }" }
26
+ # rubocop:disable Metrics/MethodLength
27
+ def projection_javascript(name, events, namespace)
28
+ callback_fmt = if namespace.nil?
29
+ <<~JS
30
+ '%{en}': function(s, e) {
31
+ linkTo('%{name}', e)
32
+ }
33
+ JS
34
+ else
35
+ <<~JS
36
+ '%{en}': function(s, e) {
37
+ if (e['metadata'] !== null && e['metadata']['namespace'] === '%{namespace}') {
38
+ linkTo('%{name}', e)
39
+ }
40
+ }
41
+ JS
42
+ end
43
+ callbacks = events.map { |en| format(callback_fmt, en: en, name: name, namespace: namespace) }
44
+
26
45
  # Alternative code using internal indexing.
27
46
  # It's broken though because it reorders events for aggregates (because the streams
28
47
  # it uses are per-event). An alternative would be to use aggregates as streams
29
48
  # to pull from.
30
49
  # et_streams = events.map { |en| "\"$et-#{en}\"" }
31
50
  # "fromStreams([#{et_streams.join(', ')}]).when({ #{callbacks.join(', ')} });"
32
- ''"
33
- // This is hard to find, so I'm leaving it here:
34
- // options({
35
- // reorderEvents: true,
36
- // processingLag: 100 //time in ms
37
- // });
38
- fromAll().when({ #{callbacks.join(', ')} });
39
- "''
51
+ <<~JS
52
+ // This is hard to find, so I'm leaving it here:
53
+ // options({
54
+ // reorderEvents: true,
55
+ // processingLag: 100 //time in ms
56
+ // });
57
+ fromAll().when({
58
+ #{callbacks.join(', ')}
59
+ });
60
+ JS
40
61
  end
62
+ # rubocop:enable Metrics/MethodLength
41
63
 
42
- def attempt_create_projection(name, event_names)
64
+ def attempt_create_projection(name, event_names, namespace)
43
65
  create_options = {
44
66
  name: name,
45
67
  emit: :yes,
@@ -48,7 +70,7 @@ module Akasha
48
70
  }
49
71
  query_string = Rack::Utils.build_query(create_options)
50
72
  @client.request(:post, "/projections/continuous?#{query_string}",
51
- projection_javascript(name, event_names),
73
+ projection_javascript(name, event_names, namespace),
52
74
  'Content-Type' => 'application/javascript')
53
75
  true
54
76
  rescue HttpClientError => e
@@ -56,9 +78,9 @@ module Akasha
56
78
  raise
57
79
  end
58
80
 
59
- def update_projection(name, event_names)
81
+ def update_projection(name, event_names, namespace)
60
82
  @client.request(:put, "/projection/#{name}/query?emit=yet",
61
- projection_javascript(name, event_names),
83
+ projection_javascript(name, event_names, namespace),
62
84
  'Content-Type' => 'application/javascript')
63
85
  end
64
86
  end
@@ -5,15 +5,20 @@ module Akasha
5
5
  class Stream
6
6
  attr_reader :name
7
7
 
8
- def initialize(client, stream_name)
8
+ # Create a stream object for accessing a ES stream.
9
+ # Does not create the underlying stream itself.
10
+ # Use the `max_retries` option to choose how many times to retry in case
11
+ def initialize(client, stream_name, max_retries: 0)
9
12
  @client = client
10
13
  @name = stream_name
14
+ @max_retries = max_retries
11
15
  end
12
16
 
13
- # Appends events to the stream.
17
+ # Appends `events` to the stream.
18
+ # of network failures.
14
19
  def write_events(events)
15
20
  return if events.empty?
16
- @client.retry_append_to_stream(@name, events)
21
+ @client.retry_append_to_stream(@name, events, max_retries: @max_retries)
17
22
  end
18
23
 
19
24
  # Reads events from the stream starting from `start` inclusive.
@@ -31,18 +36,18 @@ module Akasha
31
36
  position += events.size
32
37
  end
33
38
  else
34
- @client.retry_read_events_forward(@name, start, page_size, poll)
39
+ @client.retry_read_events_forward(@name, start, page_size, poll, max_retries: @max_retries)
35
40
  end
36
41
  end
37
42
 
38
43
  # Reads stream metadata.
39
44
  def metadata
40
- @client.retry_read_metadata(@name)
45
+ @client.retry_read_metadata(@name, max_retries: @max_retries)
41
46
  end
42
47
 
43
48
  # Updates stream metadata.
44
49
  def metadata=(metadata)
45
- @client.retry_write_metadata(@name, metadata)
50
+ @client.retry_write_metadata(@name, metadata, max_retries: @max_retries)
46
51
  end
47
52
  end
48
53
  end
@@ -26,9 +26,14 @@ module Akasha
26
26
  # Arguments:
27
27
  # `new_stream_name` - name of the new stream
28
28
  # `only` - array of event names
29
- def merge_all_by_event(into:, only:)
29
+ # `namespace` - optional namespace; if provided, the resulting stream will
30
+ # only contain events with the same metadata.namespace
31
+ def merge_all_by_event(into:, only:, namespace: nil)
30
32
  new_stream = Stream.new do |new_events|
31
- new_events.select { |event| only.include?(event.name) }
33
+ new_events.select do |event|
34
+ (namespace.nil? || namespace == event.metadata[:namespace]) &&
35
+ only.include?(event.name)
36
+ end
32
37
  end
33
38
  @streams[into] = new_stream
34
39
  @projections << new_stream
@@ -1,3 +1,3 @@
1
1
  module Akasha
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.4.0-edge-5'.freeze
3
3
  end
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: akasha
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0.pre.edge.pre.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcin Bilski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-18 00:00:00.000000000 Z
11
+ date: 2018-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: faraday
14
+ name: corefines
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.15'
19
+ version: '1.11'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.15'
26
+ version: '1.11'
27
27
  - !ruby/object:Gem::Dependency
28
- name: faraday_middleware
28
+ name: faraday
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.15'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.15'
41
41
  - !ruby/object:Gem::Dependency
42
- name: typhoeus
42
+ name: faraday_middleware
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.3'
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '1.3'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rack
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -81,19 +81,19 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: corefines
84
+ name: typhoeus
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.11'
89
+ version: '1.3'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.11'
96
+ version: '1.3'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: bundler
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -235,6 +235,7 @@ files:
235
235
  - lib/akasha/storage/http_event_store.rb
236
236
  - lib/akasha/storage/http_event_store/client.rb
237
237
  - lib/akasha/storage/http_event_store/event_serializer.rb
238
+ - lib/akasha/storage/http_event_store/exceptions.rb
238
239
  - lib/akasha/storage/http_event_store/projection_manager.rb
239
240
  - lib/akasha/storage/http_event_store/response_handler.rb
240
241
  - lib/akasha/storage/http_event_store/stream.rb
@@ -256,9 +257,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
256
257
  version: '0'
257
258
  required_rubygems_version: !ruby/object:Gem::Requirement
258
259
  requirements:
259
- - - ">="
260
+ - - ">"
260
261
  - !ruby/object:Gem::Version
261
- version: '0'
262
+ version: 1.3.1
262
263
  requirements: []
263
264
  rubyforge_project:
264
265
  rubygems_version: 2.6.11