star_gates 0.0.0 → 0.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.dictate.toml +19 -0
  3. data/.pre-commit-config.yaml +20 -0
  4. data/CHANGELOG.md +60 -3
  5. data/LICENSE.txt +23 -16
  6. data/README.md +182 -16
  7. data/Rakefile +14 -4
  8. data/lib/star_gates/adapters/base_adapter.rb +65 -0
  9. data/lib/star_gates/adapters/memory_adapter.rb +93 -0
  10. data/lib/star_gates/adapters/redis_adapter.rb +119 -0
  11. data/lib/star_gates/application_star_gate.rb +398 -0
  12. data/lib/star_gates/circuit.rb +171 -0
  13. data/lib/star_gates/configuration.rb +172 -0
  14. data/lib/star_gates/dsl.rb +71 -2
  15. data/lib/star_gates/errors.rb +8 -0
  16. data/lib/star_gates/event_store/event_store.rb +79 -0
  17. data/lib/star_gates/event_store/memory_adapter.rb +78 -0
  18. data/lib/star_gates/events/base_event.rb +45 -0
  19. data/lib/star_gates/events/feature_events.rb +97 -0
  20. data/lib/star_gates/feature.rb +426 -0
  21. data/lib/star_gates/flags.rb +153 -0
  22. data/lib/star_gates/gate.rb +100 -0
  23. data/lib/star_gates/hexagonal/feature_aggregate.rb +250 -0
  24. data/lib/star_gates/hexagonal/feature_repository.rb +67 -0
  25. data/lib/star_gates/hexagonal/feature_service.rb +121 -0
  26. data/lib/star_gates/jsonrpc_rails_adapter.rb +251 -0
  27. data/lib/star_gates/projections/feature_projection.rb +224 -0
  28. data/lib/star_gates/registry.rb +58 -0
  29. data/lib/star_gates/telemetry/opentelemetry_adapter.rb +252 -0
  30. data/lib/star_gates/telemetry/prometheus_adapter.rb +183 -0
  31. data/lib/star_gates/telemetry/telemetry_adapter.rb +160 -0
  32. data/lib/star_gates/version.rb +1 -1
  33. data/lib/star_gates.rb +308 -8
  34. metadata +95 -14
  35. data/sig/star_gates.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91627ddc25a266e8584724a1e1973799dc693e883f4b5d01dfe33cb1ccbd6fbf
4
- data.tar.gz: be8eaf8ff34249e3cf355c3fa8fd1159625aee1e23be009ce9664d644f837a6d
3
+ metadata.gz: 9150966ca03ffe47ce938c669d38ab3291e25b6b7aaea9560e89099ec255a65d
4
+ data.tar.gz: 0454b43be544e3c50026811f5d44ec3a28903c4ff824fcf3b855aaf70a733aba
5
5
  SHA512:
6
- metadata.gz: 1ff36a75a319ed7646b3eab22a9b256078044f5144c2ab7eb62f8b052504a800565ab13c3e9c5a7010428df585cbc974f9d4b0c5eb1d8fefa20049cc6b61462b
7
- data.tar.gz: df5408c183aba188434a0f74b0b330036b1951786fa3abd78b80b4eda7e7151b1270e17eaba39fb27f8422efeb25392ec0f575805c18f627053ed981056f8184
6
+ metadata.gz: 4d7802feb0b5ebc3aefcd585224cfd5dda9f95e9e8f23ee5653a087f5326d14764ce7e6a06d16ca01cafd60570325c910cb776464b2abce14b2ef05b0a202407
7
+ data.tar.gz: 3e57ca2506afe07616e9c884f1f74228c2f380601a0ca4779b311a422aa0c215d228835e9160b0689db744ec788252b752f5133703992063a06c6c7db87254d7
data/.dictate.toml ADDED
@@ -0,0 +1,19 @@
1
+ [decree.supreme]
2
+ trailing_whitespace = "deny"
3
+ tabs_vs_spaces = "spaces"
4
+ tab_width = 2
5
+ final_newline = "require"
6
+ line_endings = "lf"
7
+ blank_line_whitespace = "deny"
8
+
9
+ [decree.supreme.ignore.tab-character]
10
+ filenames = ["Makefile", "GNUmakefile", "makefile"]
11
+ extensions = ["md", "mdx", "tsv"]
12
+
13
+ [decree.ruby]
14
+ max_line_length = 120
15
+ max_lines = 400
16
+ ignore_comments = true
17
+ ignore_blank_lines = true
18
+ method_visibility_order = ["public", "protected", "private"]
19
+ comment_spacing = true
@@ -0,0 +1,20 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+ - id: check-merge-conflict
10
+ - id: check-json
11
+ - id: check-case-conflict
12
+
13
+ - repo: local
14
+ hooks:
15
+ - id: dictator
16
+ name: dictator
17
+ entry: dictator lint
18
+ language: system
19
+ types: [text]
20
+ require_serial: true
data/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2025-07-08
3
+ ## Unreleased
4
4
 
5
- - Initial release
5
+ ## 0.2.0 (2026-10-28)
6
+
7
+ ### Changed
8
+
9
+ - Use explicit rollout `key:` and `groups:` context. Group predicates receive the
10
+ full context; simulation, debugging, and scenario analysis use key-based inputs
11
+ and results. Remove user-specific helpers and telemetry labels.
12
+ - Require Ruby 4.0 or newer.
13
+ - Require StateMachines 0.202.0 or newer.
14
+ - Change the license to BSD-3-Clause.
15
+ - Use ActiveSupport duration helpers without overriding Rails behavior.
16
+ - Preserve fractional seconds in circuit cooldowns and failure windows.
17
+ - Consolidate documentation and remove generated repository artifacts, keeping the space-fleet examples.
18
+ - Move demos into the Rails dummy app and rewrite ship systems as StarGates gates.
19
+ - Run gem and Rails integration tests separately, with explicit test discovery and Rails setup.
20
+ - Use the Dictator GitHub Action and a project decree for structural linting.
21
+ - Run both test suites with padded_cell and Minitest 6.
22
+
23
+ ### Fixed
24
+
25
+ - Keep percentage rollout cohorts stable across workers and restarts with CRC32
26
+ key buckets. Upgrading reassigns cohorts previously based on Ruby's
27
+ process-randomized string hashes.
28
+ - Correct dummy-app feature routing, persistent feature transitions, and circuit statistics.
29
+ - Make weather demo failures explicit instead of random.
30
+
31
+ ### Added
32
+
33
+ - Boolean flags with environment, configuration, and runtime overrides.
34
+ - Flag status, metadata, and debugging helpers.
35
+
36
+ ## 0.1.0 (2025-10-30)
37
+
38
+ ### Security
39
+
40
+ - Use safe YAML loading for configuration files.
41
+
42
+ ### Fixed
43
+
44
+ - Correct percentage-string parsing for feature rollouts.
45
+ - Remove dead time-parsing code.
46
+
47
+ ### Added
48
+
49
+ - ChronoMachines retry policies with exponential backoff and jitter.
50
+ - Circuit retry instrumentation.
51
+ - Feature state transitions, rollout targeting, and automatic circuit-triggered rollback.
52
+ - Transition history and introspection.
53
+ - Regression coverage for configuration security, rollouts, concurrency, and retries.
54
+
55
+ ### Changed
56
+
57
+ - Use immutable `Data.define` mock users instead of `OpenStruct`.
58
+ - Use concurrent-ruby primitives for thread-safe registries and transition history.
59
+
60
+ ## 0.0.0
61
+
62
+ - Internal development version.
data/LICENSE.txt CHANGED
@@ -1,21 +1,28 @@
1
- The MIT License (MIT)
1
+ BSD 3-Clause License
2
2
 
3
3
  Copyright (c) 2025 Abdelkader Boudih
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
11
7
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
14
10
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -1,39 +1,205 @@
1
1
  # StarGates
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ [![Gem Version](https://badge.fury.io/rb/star_gates.svg)](https://badge.fury.io/rb/star_gates)
4
+ [![CI](https://github.com/seuros/star_gates/actions/workflows/ci.yml/badge.svg)](https://github.com/seuros/star_gates/actions/workflows/ci.yml)
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/star_gates`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ Feature flags, gradual rollouts, and circuit-protected execution for Ruby.
7
+ StarGates uses StateMachines for feature transitions, BreakerMachines for circuit
8
+ protection, and ChronoMachines for retries.
6
9
 
7
10
  ## Installation
8
11
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
12
+ Requires **Ruby 4.0+** and **ActiveSupport 8.1.1+**. Rails is optional.
10
13
 
11
- Install the gem and add to the application's Gemfile by executing:
14
+ ```ruby
15
+ gem 'star_gates'
16
+ ```
17
+
18
+ ```sh
19
+ bundle install
20
+ ```
21
+
22
+ ## Boolean flags
23
+
24
+ ```ruby
25
+ require 'star_gates'
26
+
27
+ StarGates.flags.configuration = { copilot: true }
28
+ StarGates.flags.enabled?(:copilot)
29
+ StarGates.flags.enable(:preview, metadata: { reason: 'testing' })
30
+ StarGates.flags.disable(:preview)
31
+ StarGates.flags.debug(:preview)
32
+ StarGates.flags.reset!
33
+ ```
34
+
35
+ Precedence: nonblank `FEATURE_<NAME>` environment variable, boolean configuration,
36
+ runtime override, then `false`. Names are case-insensitive; dashes become underscores.
37
+ Truthy strings are `true`, `1`, `yes`, and `on` (case-insensitive).
38
+
39
+ For Rails, keep defaults explicit and resolve them lazily:
40
+
41
+ ```ruby
42
+ # config/initializers/feature_flags.rb
43
+ Rails.application.config.x.feature_flags = { copilot: true }
44
+ StarGates.flags.configuration = -> { Rails.application.config.x.feature_flags }
45
+ ```
46
+
47
+ `StarGates::Flags.new(env: {}, configuration: {})` creates an isolated registry.
48
+ `all`, `enabled_flags`, `summary`, and `flag_status` expose values and their sources.
49
+ Runtime transitions reuse `StarGates::Feature`; changes and metadata are process-local,
50
+ not persisted or synchronized across workers. `reset!` clears both runtime state and
51
+ metadata, not environment/configuration. `StarGates.reset!` resets gates separately.
52
+
53
+ ## Gates and rollouts
54
+
55
+ ```ruby
56
+ StarGates.define do
57
+ gate :payments do
58
+ feature rollout: 25.percent
59
+ circuit threshold: 3, within: 1.minute, reset_after: 30.seconds
60
+
61
+ run { |context| PaymentAPI.charge(context[:amount]) }
62
+ fallback { |context| LegacyPaymentAPI.charge(context[:amount]) }
63
+ end
64
+ end
65
+
66
+ gate = StarGates[:payments]
67
+ gate.feature.rollout_canary
68
+ gate.call(key: 'account:42', amount: 100)
69
+ ```
70
+
71
+ Features start disabled. Use `rollout_canary` for percentage/group targeting,
72
+ `enable` for full rollout, and `disable` to turn a feature off. Pass a stable
73
+ string or integer `key:` for percentage targeting, such as an account, resident,
74
+ or device identifier.
75
+
76
+ Percentage targeting stays consistent across workers and restarts. Increasing
77
+ the rollout percentage retains keys already included.
78
+
79
+ For group targeting, pass `groups: [:beta]` to match `feature group: :beta`.
80
+ A predicate such as `feature group: ->(context) { context[:region] == 'eu' }`
81
+ receives the full context passed to the gate.
82
+
83
+ When a circuit opens, its feature rolls back from enabled to canary, or from canary
84
+ to disabled. Gates use their fallback when the feature is unavailable or execution
85
+ fails. Rails duration helpers retain their normal behavior, including fractional
86
+ seconds such as `0.5.seconds`.
87
+
88
+ ### Retries
89
+
90
+ Pass `retry_config` to a gate's `circuit` declaration:
91
+
92
+ ```ruby
93
+ circuit threshold: 5,
94
+ within: 1.minute,
95
+ retry_config: {
96
+ max_attempts: 3,
97
+ base_delay: 0.1,
98
+ multiplier: 2,
99
+ max_delay: 5
100
+ }
101
+ ```
102
+
103
+ Retries are opt-in and use ChronoMachines.
104
+
105
+ ## Rails
106
+
107
+ Define gates in an initializer and call them from controllers or services:
108
+
109
+ ```ruby
110
+ class PaymentsController < ApplicationController
111
+ def create
112
+ result = StarGates[:payments].call(key: "account:#{current_account.id}", amount: params[:amount])
113
+ render json: result
114
+ end
115
+ end
116
+ ```
117
+
118
+ The Rails integration fixture lives in `test/dummy/` and uses the root Gemfile.
119
+
120
+ ## Instrumentation
121
+
122
+ StarGates emits events through `ActiveSupport::Notifications`:
123
+
124
+ ```ruby
125
+ ActiveSupport::Notifications.subscribe('star_gates.circuit.auto_rollback') do |*args|
126
+ event = ActiveSupport::Notifications::Event.new(*args)
127
+ Rails.logger.warn(event.payload)
128
+ end
129
+ ```
130
+
131
+ Circuit events include `circuit.auto_rollback`, `circuit.half_open`,
132
+ `circuit.recovered`, and `circuit.retry`, prefixed with `star_gates.`.
12
133
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
134
+ ## Examples
135
+
136
+ ### Space fleet
137
+
138
+ All demos live in `test/dummy/`. The fleet uses real `StarGates::Gate` instances
139
+ for feature rollout, circuit protection, automatic rollback, and fallbacks.
140
+ BreakerMachines is the underlying circuit engine, not a separate ship API.
141
+
142
+ - [Spaceship](test/dummy/app/models/spaceship.rb): warp drive, fusion reactor, and navigation circuits.
143
+ - [Spaceflight systems](test/dummy/app/models/spaceflight_systems.rb): simulated ship subsystems.
144
+ - [Ship hierarchy](test/dummy/app/models/base_ship.rb): shared systems for the fleet.
145
+ - [Corellian freighter](test/dummy/app/models/corellian_freighter.rb): specialized ship behavior.
146
+ - [RMNS Atlas Monkey](test/dummy/app/models/rmns_atlas_monkey.rb): the Universal Commentary Engine.
147
+
148
+ Run the fleet demo without starting Rails:
149
+
150
+ ```sh
151
+ bundle exec ruby test/dummy/demos/space_fleet.rb
15
152
  ```
16
153
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
154
+ It demonstrates disabled-system fallback, pilot-only canary access, full rollout,
155
+ hardware failures opening a circuit, automatic feature rollback, and explicit
156
+ operator-approved recovery. Each ship's gates have independent state.
157
+
158
+ ```ruby
159
+ require_relative 'test/dummy/app/models/spaceship'
18
160
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
161
+ ship = Spaceship.new('SG Atlas', 'SG-1')
162
+ warp = ship.gate(:warp_drive) # A StarGates::Gate, with a feature and a circuit.
163
+ warp.feature.rollout_canary
164
+ ship.engage_warp('Alpha Centauri', key: 'crew:1', groups: [:pilots])
21
165
  ```
22
166
 
23
- ## Usage
167
+ ### Application examples
24
168
 
25
- TODO: Write usage instructions here
169
+ - [Payment feature](test/dummy/demos/payment_feature.rb): state-specific feature behavior.
170
+ - [Transition history](test/dummy/demos/transition_demo.rb): recorded feature transitions.
171
+ - [Introspection](test/dummy/demos/introspection_demo.rb): status, analytics, and context simulation.
172
+ - [Application payment feature](test/dummy/demos/application_payment_feature.rb): application-level features.
173
+ - [JSON-RPC controller](test/dummy/demos/jsonrpc_rails_controller.rb): optional JSON-RPC integration.
174
+
175
+ Run standalone examples from the repository root:
176
+
177
+ ```sh
178
+ bundle exec ruby test/dummy/demos/payment_feature.rb
179
+ ```
26
180
 
27
181
  ## Development
28
182
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
183
+ ```sh
184
+ bin/setup
185
+ bundle exec rake test
186
+ bundle exec rake test:rails
187
+ dictator lint .
188
+ ```
30
189
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
190
+ Tests run with padded_cell and Minitest 6. Gem tests and Rails integration tests
191
+ run in separate processes. `bundle exec rake` runs both suites. To select gem tests
192
+ directly or pass runner options:
32
193
 
33
- ## Contributing
194
+ ```sh
195
+ bundle exec padded_cell test/gate_test.rb
196
+ bundle exec rake test TESTOPTS='-j 4 --seed 1234'
197
+ ```
34
198
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/star_gates.
199
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
36
200
 
37
201
  ## License
38
202
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
203
+ [BSD-3-Clause](LICENSE.txt).
204
+
205
+ > In space, nobody can hear your feature flags fail, but they'll notice when your fleet survives.
data/Rakefile CHANGED
@@ -1,8 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'shellwords'
5
5
 
6
- Minitest::TestTask.create
6
+ desc 'Run gem tests with padded_cell'
7
+ task :test do
8
+ sh 'padded_cell', 'test/*_test.rb', *Shellwords.split(ENV.fetch('TESTOPTS', ''))
9
+ end
7
10
 
8
- task default: :test
11
+ namespace :test do
12
+ desc 'Run Rails integration tests with padded_cell'
13
+ task :rails do
14
+ sh 'padded_cell', 'test/dummy/test', *Shellwords.split(ENV.fetch('TESTOPTS', ''))
15
+ end
16
+ end
17
+
18
+ task default: [:test, 'test:rails']
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StarGates
4
+ module Adapters
5
+ class BaseAdapter
6
+ def initialize(config = {})
7
+ @config = config
8
+ end
9
+
10
+ # Persistence operations
11
+ def store(key, value)
12
+ raise NotImplementedError, 'Subclasses must implement #store'
13
+ end
14
+
15
+ def fetch(key)
16
+ raise NotImplementedError, 'Subclasses must implement #fetch'
17
+ end
18
+
19
+ def delete(key)
20
+ raise NotImplementedError, 'Subclasses must implement #delete'
21
+ end
22
+
23
+ def exists?(key)
24
+ raise NotImplementedError, 'Subclasses must implement #exists?'
25
+ end
26
+
27
+ # Caching operations
28
+ def cache_fetch(key, expires_in: nil, &block)
29
+ if exists?(key)
30
+ fetch(key)
31
+ elsif block_given?
32
+ value = block.call
33
+ store(key, value) if value
34
+ value
35
+ end
36
+ end
37
+
38
+ def cache_write(key, value, expires_in: nil)
39
+ store(key, value)
40
+ end
41
+
42
+ def cache_delete(key)
43
+ delete(key)
44
+ end
45
+
46
+ # Messaging operations
47
+ def publish(topic, message)
48
+ # Default no-op implementation
49
+ end
50
+
51
+ def subscribe(topic, &)
52
+ # Default no-op implementation
53
+ end
54
+
55
+ # Health check
56
+ def healthy?
57
+ true
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :config
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_adapter'
4
+ require 'concurrent'
5
+
6
+ module StarGates
7
+ module Adapters
8
+ class MemoryAdapter < BaseAdapter
9
+ def initialize(config = {})
10
+ super
11
+ @storage = Concurrent::Hash.new
12
+ @subscribers = Concurrent::Hash.new { |h, k| h[k] = Concurrent::Array.new }
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def store(key, value)
17
+ @storage[key] = value
18
+ end
19
+
20
+ def fetch(key)
21
+ @storage[key]
22
+ end
23
+
24
+ def delete(key)
25
+ @storage.delete(key)
26
+ end
27
+
28
+ def exists?(key)
29
+ @storage.key?(key)
30
+ end
31
+
32
+ def cache_fetch(key, expires_in: nil, &block)
33
+ # Memory adapter doesn't support expiration, but we can implement basic caching
34
+ if @storage.key?(key)
35
+ @storage[key]
36
+ elsif block_given?
37
+ value = block.call
38
+ @storage[key] = value if value
39
+ value
40
+ end
41
+ end
42
+
43
+ def cache_write(key, value, expires_in: nil)
44
+ @storage[key] = value
45
+ end
46
+
47
+ def cache_delete(key)
48
+ @storage.delete(key)
49
+ end
50
+
51
+ def publish(topic, message)
52
+ subscribers = @subscribers[topic].to_a
53
+ return if subscribers.empty?
54
+
55
+ # Publish to subscribers in background fibers if available
56
+ if defined?(Fiber.scheduler) && Fiber.scheduler
57
+ subscribers.each do |subscriber|
58
+ Fiber.schedule { subscriber.call(message) }
59
+ end
60
+ else
61
+ subscribers.each { |subscriber| subscriber.call(message) }
62
+ end
63
+ end
64
+
65
+ def subscribe(topic, &block)
66
+ @subscribers[topic] << block
67
+ end
68
+
69
+ def clear!
70
+ @mutex.synchronize do
71
+ @storage.clear
72
+ @subscribers.clear
73
+ end
74
+ end
75
+
76
+ def size
77
+ @storage.size
78
+ end
79
+
80
+ def topics_count
81
+ @subscribers.size
82
+ end
83
+
84
+ def subscribers_count(topic)
85
+ @subscribers[topic].size
86
+ end
87
+
88
+ def healthy?
89
+ true
90
+ end
91
+ end
92
+ end
93
+ end