solid_objects 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 +7 -0
- data/CHANGELOG.md +17 -0
- data/MIT-LICENSE +19 -0
- data/README.md +744 -0
- data/Rakefile +40 -0
- data/app/controllers/solid_objects/application_controller.rb +23 -0
- data/app/controllers/solid_objects/dead_letters_controller.rb +23 -0
- data/app/controllers/solid_objects/instances_controller.rb +29 -0
- data/app/helpers/solid_objects/actor_helper.rb +25 -0
- data/app/models/solid_objects/broadcast.rb +10 -0
- data/app/models/solid_objects/claimed_message.rb +14 -0
- data/app/models/solid_objects/dead_letter.rb +13 -0
- data/app/models/solid_objects/effect.rb +10 -0
- data/app/models/solid_objects/instance.rb +93 -0
- data/app/models/solid_objects/message.rb +53 -0
- data/app/models/solid_objects/process.rb +13 -0
- data/app/models/solid_objects/ready_message.rb +10 -0
- data/app/models/solid_objects/record.rb +17 -0
- data/app/models/solid_objects/reminder.rb +9 -0
- data/app/views/solid_objects/dead_letters/index.html.erb +26 -0
- data/app/views/solid_objects/instances/index.html.erb +24 -0
- data/app/views/solid_objects/instances/show.html.erb +35 -0
- data/benchmark/activation_cache.rb +5 -0
- data/benchmark/ask_latency.rb +5 -0
- data/benchmark/claim.rb +5 -0
- data/benchmark/cold_actors.rb +5 -0
- data/benchmark/concurrent_actors.rb +5 -0
- data/benchmark/enqueue.rb +5 -0
- data/benchmark/hot_actor.rb +5 -0
- data/benchmark/processing.rb +5 -0
- data/benchmark/query_count.rb +5 -0
- data/benchmark/support.rb +271 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20260805000000_create_solid_objects_tables.rb +319 -0
- data/docs/adr/0001-postgresql-backend.md +21 -0
- data/docs/adr/0002-jsonb-actor-state.md +21 -0
- data/docs/adr/0003-mailbox-ordering.md +30 -0
- data/docs/adr/0004-activation-leasing.md +21 -0
- data/docs/adr/0005-fencing-tokens.md +25 -0
- data/docs/adr/0006-at-least-once-delivery.md +24 -0
- data/docs/adr/0007-transactional-outbox.md +21 -0
- data/docs/adr/0008-actor-communication.md +21 -0
- data/docs/adr/0009-realtime-updates.md +21 -0
- data/docs/adr/0010-state-versioning.md +29 -0
- data/docs/adr/0011-wake-up-strategy.md +34 -0
- data/docs/adr/0012-not-active-jobs.md +21 -0
- data/docs/adr/0013-database-adapters.md +48 -0
- data/docs/architecture.md +615 -0
- data/docs/benchmarks.md +26 -0
- data/docs/correctness.md +124 -0
- data/docs/database-schema.md +111 -0
- data/docs/development.md +87 -0
- data/docs/implementation-plan.md +518 -0
- data/docs/operations.md +123 -0
- data/docs/realtime.md +51 -0
- data/docs/research/solid_queue.md +545 -0
- data/docs/roadmap.md +53 -0
- data/docs/security.md +61 -0
- data/docs/state-migrations.md +46 -0
- data/examples/application/README.md +16 -0
- data/examples/application/app/actors/chat_room_actor.rb +34 -0
- data/examples/application/app/actors/shopping_cart_actor.rb +79 -0
- data/examples/application/app/controllers/cart_controller.rb +54 -0
- data/examples/application/app/controllers/chat_rooms_controller.rb +44 -0
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +8 -0
- data/examples/application/app/views/actors/shopping_cart_actor/_summary.html.erb +10 -0
- data/examples/application/app/views/cart/show.html.erb +13 -0
- data/examples/application/app/views/chat_rooms/show.html.erb +8 -0
- data/examples/application/config/initializers/solid_objects.rb +23 -0
- data/examples/application/config/routes.rb +20 -0
- data/exe/solid_objects +9 -0
- data/lib/generators/solid_objects/install_generator.rb +21 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +13 -0
- data/lib/solid_objects/action_cable_broadcast_adapter.rb +19 -0
- data/lib/solid_objects/activation.rb +183 -0
- data/lib/solid_objects/activation_manager.rb +102 -0
- data/lib/solid_objects/actor.rb +271 -0
- data/lib/solid_objects/actor_channel.rb +29 -0
- data/lib/solid_objects/actor_definition.rb +212 -0
- data/lib/solid_objects/actor_registry.rb +65 -0
- data/lib/solid_objects/actor_snapshot.rb +42 -0
- data/lib/solid_objects/actor_view.rb +117 -0
- data/lib/solid_objects/broadcast_executor.rb +162 -0
- data/lib/solid_objects/cli.rb +118 -0
- data/lib/solid_objects/client.rb +153 -0
- data/lib/solid_objects/configuration.rb +168 -0
- data/lib/solid_objects/context.rb +41 -0
- data/lib/solid_objects/database_adapter.rb +82 -0
- data/lib/solid_objects/database_adapters/mysql.rb +22 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +17 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +12 -0
- data/lib/solid_objects/dead_letter_manager.rb +47 -0
- data/lib/solid_objects/dom_identity.rb +38 -0
- data/lib/solid_objects/effect_executor.rb +235 -0
- data/lib/solid_objects/effect_registry.rb +34 -0
- data/lib/solid_objects/engine.rb +33 -0
- data/lib/solid_objects/errors.rb +65 -0
- data/lib/solid_objects/executor.rb +290 -0
- data/lib/solid_objects/instrumentation.rb +10 -0
- data/lib/solid_objects/lease.rb +172 -0
- data/lib/solid_objects/lease_renewer.rb +70 -0
- data/lib/solid_objects/log_subscriber.rb +29 -0
- data/lib/solid_objects/mailbox.rb +178 -0
- data/lib/solid_objects/message_reference.rb +52 -0
- data/lib/solid_objects/process_registry.rb +143 -0
- data/lib/solid_objects/reference.rb +96 -0
- data/lib/solid_objects/reminder_scheduler.rb +168 -0
- data/lib/solid_objects/serialization.rb +99 -0
- data/lib/solid_objects/state.rb +111 -0
- data/lib/solid_objects/stream_name.rb +29 -0
- data/lib/solid_objects/stream_token.rb +59 -0
- data/lib/solid_objects/supervisor.rb +87 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +35 -0
- data/lib/solid_objects/version.rb +5 -0
- data/lib/solid_objects/wake_up.rb +28 -0
- data/lib/solid_objects/worker.rb +139 -0
- data/lib/solid_objects.rb +118 -0
- data/sig/generated/controllers/solid_objects/application_controller.rbs +10 -0
- data/sig/generated/controllers/solid_objects/dead_letters_controller.rbs +11 -0
- data/sig/generated/controllers/solid_objects/instances_controller.rbs +11 -0
- data/sig/generated/helpers/solid_objects/actor_helper.rbs +8 -0
- data/sig/generated/lib/generators/solid_objects/install_generator.rbs +13 -0
- data/sig/generated/lib/solid_objects/action_cable_broadcast_adapter.rbs +8 -0
- data/sig/generated/lib/solid_objects/activation.rbs +65 -0
- data/sig/generated/lib/solid_objects/activation_manager.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor.rbs +183 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +8 -0
- data/sig/generated/lib/solid_objects/actor_definition.rbs +117 -0
- data/sig/generated/lib/solid_objects/actor_registry.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +28 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +56 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +55 -0
- data/sig/generated/lib/solid_objects/cli.rbs +31 -0
- data/sig/generated/lib/solid_objects/client.rbs +35 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +147 -0
- data/sig/generated/lib/solid_objects/context.rbs +56 -0
- data/sig/generated/lib/solid_objects/database_adapter.rbs +42 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +16 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +13 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +10 -0
- data/sig/generated/lib/solid_objects/dead_letter_manager.rbs +16 -0
- data/sig/generated/lib/solid_objects/dom_identity.rbs +20 -0
- data/sig/generated/lib/solid_objects/effect_executor.rbs +82 -0
- data/sig/generated/lib/solid_objects/effect_registry.rbs +24 -0
- data/sig/generated/lib/solid_objects/engine.rbs +7 -0
- data/sig/generated/lib/solid_objects/errors.rbs +64 -0
- data/sig/generated/lib/solid_objects/executor.rbs +60 -0
- data/sig/generated/lib/solid_objects/instrumentation.rbs +8 -0
- data/sig/generated/lib/solid_objects/lease.rbs +57 -0
- data/sig/generated/lib/solid_objects/lease_renewer.rbs +42 -0
- data/sig/generated/lib/solid_objects/log_subscriber.rbs +11 -0
- data/sig/generated/lib/solid_objects/mailbox.rbs +46 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +37 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +46 -0
- data/sig/generated/lib/solid_objects/reference.rbs +45 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +55 -0
- data/sig/generated/lib/solid_objects/serialization.rbs +31 -0
- data/sig/generated/lib/solid_objects/state.rbs +72 -0
- data/sig/generated/lib/solid_objects/stream_name.rbs +11 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +19 -0
- data/sig/generated/lib/solid_objects/supervisor.rbs +38 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +14 -0
- data/sig/generated/lib/solid_objects/version.rbs +5 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +24 -0
- data/sig/generated/lib/solid_objects/worker.rbs +56 -0
- data/sig/generated/lib/solid_objects.rbs +41 -0
- data/sig/generated/models/solid_objects/broadcast.rbs +6 -0
- data/sig/generated/models/solid_objects/claimed_message.rbs +6 -0
- data/sig/generated/models/solid_objects/dead_letter.rbs +6 -0
- data/sig/generated/models/solid_objects/effect.rbs +6 -0
- data/sig/generated/models/solid_objects/instance.rbs +25 -0
- data/sig/generated/models/solid_objects/message.rbs +22 -0
- data/sig/generated/models/solid_objects/process.rbs +6 -0
- data/sig/generated/models/solid_objects/ready_message.rbs +6 -0
- data/sig/generated/models/solid_objects/record.rbs +8 -0
- data/sig/generated/models/solid_objects/reminder.rbs +6 -0
- data/sig/support/framework.rbs +37 -0
- metadata +467 -0
data/docs/security.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Security guide
|
|
2
|
+
|
|
3
|
+
## Policy hooks
|
|
4
|
+
|
|
5
|
+
Solid Objects has separate hooks for sending messages, querying state,
|
|
6
|
+
destroying actors, subscribing to actor streams, and administration. The host
|
|
7
|
+
application supplies the authenticated request or connection as
|
|
8
|
+
`authorization_context`. All five hooks deny by default.
|
|
9
|
+
|
|
10
|
+
Method-style reference calls do not bypass these hooks. Public instance methods
|
|
11
|
+
declared on an actor are part of its remotely addressable message surface and
|
|
12
|
+
delegate to the authorized `tell` path. Keep implementation helpers private or
|
|
13
|
+
protected. Query and attribute methods delegate to the authorized `ask` path.
|
|
14
|
+
`reference.destroy` delegates to `authorize_destroy` before checking whether
|
|
15
|
+
the actor exists, so denial does not reveal actor existence.
|
|
16
|
+
|
|
17
|
+
Internal runtime delivery bypasses the public client only for rows already
|
|
18
|
+
created by a committed actor turn. It never converts a database actor type into
|
|
19
|
+
a Ruby constant.
|
|
20
|
+
|
|
21
|
+
## Actor identity
|
|
22
|
+
|
|
23
|
+
Actor IDs are identifiers, not capabilities. Treat user-supplied IDs as
|
|
24
|
+
untrusted and verify tenancy/ownership in every public policy hook. The registry
|
|
25
|
+
maps explicit type strings to actor classes; arbitrary constantization is
|
|
26
|
+
forbidden.
|
|
27
|
+
|
|
28
|
+
Opaque stream and DOM names reduce accidental disclosure but do not replace
|
|
29
|
+
authorization. Signed stream tokens are readable by their recipient and prove
|
|
30
|
+
integrity only.
|
|
31
|
+
|
|
32
|
+
## Serialization
|
|
33
|
+
|
|
34
|
+
The built-in serializer accepts JSON-compatible data, normalizes keys to
|
|
35
|
+
strings, rejects colliding normalized keys, limits nesting and byte size, and
|
|
36
|
+
rejects non-finite numbers. It never loads Ruby objects with `Marshal`.
|
|
37
|
+
|
|
38
|
+
Database access is still privileged input. Restrict who can write runtime
|
|
39
|
+
tables, because a malicious row can request registered messages or effects even
|
|
40
|
+
without unsafe object deserialization.
|
|
41
|
+
|
|
42
|
+
## Administration
|
|
43
|
+
|
|
44
|
+
The engine controllers and dead-letter service deny access unless
|
|
45
|
+
`authorize_administration` approves. Actor state, arguments, results, errors,
|
|
46
|
+
and backtraces may contain sensitive application data. Keep admin routes behind
|
|
47
|
+
host authentication and audit their use.
|
|
48
|
+
|
|
49
|
+
Instrumentation excludes arguments, state, results, and effect payloads by
|
|
50
|
+
default. Review custom logging and effect handlers for accidental disclosure.
|
|
51
|
+
|
|
52
|
+
Actor destruction is not an administrative shortcut. Authorize tenancy and
|
|
53
|
+
ownership explicitly in `authorize_destroy`; knowledge of an actor ID is never
|
|
54
|
+
permission to delete its state or queued work.
|
|
55
|
+
|
|
56
|
+
## Denial of service
|
|
57
|
+
|
|
58
|
+
Configure mailbox and byte limits. Add host rate limiting before public actor
|
|
59
|
+
calls. One actor is intentionally sequential, so an attacker who can target a
|
|
60
|
+
single identity can create a hot-actor bottleneck even when the global worker
|
|
61
|
+
pool has capacity.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# State migration guide
|
|
2
|
+
|
|
3
|
+
Declare a monotonically increasing version and every adjacent migration:
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
class ShoppingCartActor < SolidObjects::Actor
|
|
7
|
+
state_version 2
|
|
8
|
+
|
|
9
|
+
migrate_state from: 1, to: 2 do |state|
|
|
10
|
+
state["currency"] ||= "USD"
|
|
11
|
+
state
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Migration runs in memory during activation. The new version is persisted only
|
|
17
|
+
with the next successful fenced message commit.
|
|
18
|
+
|
|
19
|
+
## Runtime rules
|
|
20
|
+
|
|
21
|
+
- Stored state newer than running code refuses activation.
|
|
22
|
+
- Missing migration steps fail activation.
|
|
23
|
+
- Every migration result must pass JSON serialization.
|
|
24
|
+
- Additive representation changes that old and new code both understand need
|
|
25
|
+
no version bump.
|
|
26
|
+
- Destructive changes require expand/contract releases.
|
|
27
|
+
- Published actor migrations are never squashed. A year-idle actor may still
|
|
28
|
+
hold a year-old blob.
|
|
29
|
+
|
|
30
|
+
## Rolling deploys
|
|
31
|
+
|
|
32
|
+
A new worker can migrate state while an old worker still exists. If the old code
|
|
33
|
+
cannot read the migrated shape, drain the old workers before allowing the new
|
|
34
|
+
version to persist it.
|
|
35
|
+
|
|
36
|
+
A safe destructive rollout normally uses:
|
|
37
|
+
|
|
38
|
+
1. Expand readers to accept old and new shapes.
|
|
39
|
+
2. Deploy that compatibility release everywhere.
|
|
40
|
+
3. Add and enable the actor migration.
|
|
41
|
+
4. Allow active and dormant actors to migrate over time, or run an authorized
|
|
42
|
+
message-based migration campaign.
|
|
43
|
+
5. Remove old-shape support only after operational evidence says it is safe.
|
|
44
|
+
|
|
45
|
+
Never update actor JSON in a bulk SQL migration. Use actor messages so fencing,
|
|
46
|
+
ordering, observables, and outboxes remain intact.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Example application source
|
|
2
|
+
|
|
3
|
+
This directory contains the host-application pieces for the shopping cart and
|
|
4
|
+
chat room examples. Copy them into a Rails application that has run
|
|
5
|
+
`bin/rails generate solid_objects:install`, mount the engine for administration,
|
|
6
|
+
and start the runtime with `bundle exec solid_objects start`.
|
|
7
|
+
|
|
8
|
+
The payment adapter receives the durable effect ID as its provider idempotency
|
|
9
|
+
key. The chat actor also gives every submitted chat message a caller-generated
|
|
10
|
+
message ID and checks that ID in durable actor state, because actor handlers may
|
|
11
|
+
be redelivered.
|
|
12
|
+
|
|
13
|
+
The views demonstrate initial `solid_object` rendering and live observable
|
|
14
|
+
replacement. Live cart-summary replacement and chat-message append are
|
|
15
|
+
deliberately not shown as working behavior because component broadcasts and
|
|
16
|
+
Turbo append intents remain roadmap items.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class ChatRoomActor < SolidObjects::Actor
|
|
4
|
+
MAX_RECENT_MESSAGES = 100
|
|
5
|
+
|
|
6
|
+
attribute :members, default: -> { [] }
|
|
7
|
+
attribute :recent_messages, default: -> { [] }
|
|
8
|
+
|
|
9
|
+
observable :presence do
|
|
10
|
+
members.length
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
observable :recent_messages
|
|
14
|
+
|
|
15
|
+
def join(user_id:)
|
|
16
|
+
members << user_id unless members.include?(user_id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def leave(user_id:)
|
|
20
|
+
members.delete(user_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send_message(message_id:, user_id:, body:)
|
|
24
|
+
return if recent_messages.any? { |message| message.fetch("id") == message_id }
|
|
25
|
+
return unless members.include?(user_id)
|
|
26
|
+
|
|
27
|
+
recent_messages << {
|
|
28
|
+
"id" => message_id,
|
|
29
|
+
"user_id" => user_id,
|
|
30
|
+
"body" => body
|
|
31
|
+
}
|
|
32
|
+
recent_messages.shift while recent_messages.length > MAX_RECENT_MESSAGES
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class ShoppingCartActor < SolidObjects::Actor
|
|
4
|
+
attribute :items, default: -> { [] }
|
|
5
|
+
attribute :checkout_status, default: "open"
|
|
6
|
+
attribute :payment_id
|
|
7
|
+
|
|
8
|
+
observable :items_count do
|
|
9
|
+
items.sum { |item| item.fetch("quantity") }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
observable :subtotal_cents do
|
|
13
|
+
items.sum do |item|
|
|
14
|
+
item.fetch("quantity") * item.fetch("unit_price_cents")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
observable :checkout_status
|
|
19
|
+
|
|
20
|
+
def add_item(product_id:, unit_price_cents:, quantity: 1)
|
|
21
|
+
item = items.find do |candidate|
|
|
22
|
+
candidate.fetch("product_id") == product_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if item
|
|
26
|
+
item["quantity"] += quantity
|
|
27
|
+
else
|
|
28
|
+
items << {
|
|
29
|
+
"product_id" => product_id,
|
|
30
|
+
"unit_price_cents" => unit_price_cents,
|
|
31
|
+
"quantity" => quantity
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_item(product_id:)
|
|
37
|
+
items.reject! do |item|
|
|
38
|
+
item.fetch("product_id") == product_id
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def change_quantity(product_id:, quantity:)
|
|
43
|
+
item = items.find do |candidate|
|
|
44
|
+
candidate.fetch("product_id") == product_id
|
|
45
|
+
end
|
|
46
|
+
return unless item
|
|
47
|
+
|
|
48
|
+
item["quantity"] = quantity
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def checkout(payment_id:)
|
|
52
|
+
return unless checkout_status == "open"
|
|
53
|
+
|
|
54
|
+
self.checkout_status = "pending"
|
|
55
|
+
self.payment_id = payment_id
|
|
56
|
+
emit(
|
|
57
|
+
:charge_payment,
|
|
58
|
+
payment_id:,
|
|
59
|
+
amount_cents: items.sum do |item|
|
|
60
|
+
item.fetch("quantity") * item.fetch("unit_price_cents")
|
|
61
|
+
end,
|
|
62
|
+
on_success: :payment_succeeded,
|
|
63
|
+
on_failure: :payment_failed
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def payment_succeeded(effect_id:, result:)
|
|
68
|
+
return unless checkout_status == "pending"
|
|
69
|
+
return unless result.fetch("payment_id") == payment_id
|
|
70
|
+
|
|
71
|
+
self.checkout_status = "paid"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def payment_failed(effect_id:, error:)
|
|
75
|
+
return unless checkout_status == "pending"
|
|
76
|
+
|
|
77
|
+
self.checkout_status = "payment_failed"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class CartController < ApplicationController
|
|
4
|
+
# @rbs () -> void
|
|
5
|
+
def show
|
|
6
|
+
@cart = ShoppingCartActor.ref(current_user.id)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @rbs () -> void
|
|
10
|
+
def add_item
|
|
11
|
+
ShoppingCartActor.ref(current_user.id).add_item(
|
|
12
|
+
product_id: params.require(:product_id),
|
|
13
|
+
unit_price_cents: params.require(:unit_price_cents).to_i,
|
|
14
|
+
quantity: params.fetch(:quantity, 1).to_i,
|
|
15
|
+
authorization_context: self
|
|
16
|
+
)
|
|
17
|
+
redirect_to cart_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs () -> void
|
|
21
|
+
def remove_item
|
|
22
|
+
current_cart.remove_item(
|
|
23
|
+
product_id: params.require(:product_id),
|
|
24
|
+
authorization_context: self
|
|
25
|
+
)
|
|
26
|
+
redirect_to cart_path
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @rbs () -> void
|
|
30
|
+
def change_quantity
|
|
31
|
+
current_cart.change_quantity(
|
|
32
|
+
product_id: params.require(:product_id),
|
|
33
|
+
quantity: params.require(:quantity).to_i,
|
|
34
|
+
authorization_context: self
|
|
35
|
+
)
|
|
36
|
+
redirect_to cart_path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs () -> void
|
|
40
|
+
def checkout
|
|
41
|
+
current_cart.checkout(
|
|
42
|
+
payment_id: SecureRandom.uuid,
|
|
43
|
+
authorization_context: self
|
|
44
|
+
)
|
|
45
|
+
redirect_to cart_path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @rbs () -> SolidObjects::Reference
|
|
51
|
+
def current_cart
|
|
52
|
+
ShoppingCartActor.ref(current_user.id)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class ChatRoomsController < ApplicationController
|
|
4
|
+
# @rbs () -> void
|
|
5
|
+
def show
|
|
6
|
+
@room = current_room
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @rbs () -> void
|
|
10
|
+
def join
|
|
11
|
+
current_room.join(
|
|
12
|
+
user_id: current_user.id.to_s,
|
|
13
|
+
authorization_context: self
|
|
14
|
+
)
|
|
15
|
+
redirect_to chat_room_path(params[:id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs () -> void
|
|
19
|
+
def leave
|
|
20
|
+
current_room.leave(
|
|
21
|
+
user_id: current_user.id.to_s,
|
|
22
|
+
authorization_context: self
|
|
23
|
+
)
|
|
24
|
+
redirect_to chat_room_path(params[:id])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs () -> void
|
|
28
|
+
def create_message
|
|
29
|
+
current_room.send_message(
|
|
30
|
+
message_id: SecureRandom.uuid,
|
|
31
|
+
user_id: current_user.id.to_s,
|
|
32
|
+
body: params.require(:body),
|
|
33
|
+
authorization_context: self
|
|
34
|
+
)
|
|
35
|
+
redirect_to chat_room_path(params[:id])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @rbs () -> SolidObjects::Reference
|
|
41
|
+
def current_room
|
|
42
|
+
ChatRoomActor.ref(params.require(:id))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
SolidObjects.configure do |configuration|
|
|
4
|
+
configuration.authorize_message = lambda do |actor_type:, actor_id:, authorization_context:, **|
|
|
5
|
+
next false unless authorization_context.respond_to?(:current_user)
|
|
6
|
+
|
|
7
|
+
if actor_type == ShoppingCartActor.actor_type
|
|
8
|
+
authorization_context.current_user.id.to_s == actor_id
|
|
9
|
+
else
|
|
10
|
+
ChatRoomPolicy.new(authorization_context.current_user).access?(actor_id)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
configuration.authorize_query = configuration.authorize_message
|
|
14
|
+
configuration.authorize_subscription = configuration.authorize_message
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
SolidObjects.register_effect(:charge_payment) do |arguments, context|
|
|
18
|
+
Payments.charge(
|
|
19
|
+
idempotency_key: context.id,
|
|
20
|
+
payment_id: arguments.fetch("payment_id"),
|
|
21
|
+
amount_cents: arguments.fetch("amount_cents")
|
|
22
|
+
)
|
|
23
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
Rails.application.routes.draw do
|
|
4
|
+
resource :cart, only: :show do
|
|
5
|
+
post :add_item
|
|
6
|
+
post :remove_item
|
|
7
|
+
post :change_quantity
|
|
8
|
+
post :checkout
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
resources :chat_rooms, only: :show do
|
|
12
|
+
member do
|
|
13
|
+
post :join
|
|
14
|
+
post :leave
|
|
15
|
+
post :create_message
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
mount SolidObjects::Engine => "/solid_objects"
|
|
20
|
+
end
|
data/exe/solid_objects
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module SolidObjects
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
# @rbs () -> void
|
|
11
|
+
def copy_initializer
|
|
12
|
+
template "solid_objects.rb", "config/initializers/solid_objects.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
def copy_migrations
|
|
17
|
+
rake "railties:install:migrations FROM=solid_objects"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
SolidObjects.configure do |configuration|
|
|
4
|
+
configuration.worker_count = 1
|
|
5
|
+
configuration.effect_worker_count = 1
|
|
6
|
+
configuration.broadcast_worker_count = 1
|
|
7
|
+
configuration.reminder_scheduler_count = 1
|
|
8
|
+
configuration.authorize_message = ->(**) { false }
|
|
9
|
+
configuration.authorize_query = ->(**) { false }
|
|
10
|
+
configuration.authorize_destroy = ->(**) { false }
|
|
11
|
+
configuration.authorize_subscription = ->(**) { false }
|
|
12
|
+
configuration.authorize_administration = ->(**) { false }
|
|
13
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
require "action_cable/engine"
|
|
4
|
+
|
|
5
|
+
module SolidObjects
|
|
6
|
+
class ActionCableBroadcastAdapter
|
|
7
|
+
# @rbs (Broadcast) -> void
|
|
8
|
+
def call(broadcast)
|
|
9
|
+
reference = Reference.new(
|
|
10
|
+
actor_type: broadcast.instance.actor_type,
|
|
11
|
+
actor_id: broadcast.instance.actor_id
|
|
12
|
+
)
|
|
13
|
+
ActionCable.server.broadcast(
|
|
14
|
+
StreamName.for(reference),
|
|
15
|
+
TurboStreamRenderer.observable(broadcast)
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class Activation
|
|
5
|
+
# @rbs @lease: Lease
|
|
6
|
+
# @rbs @actor_class: Class
|
|
7
|
+
# @rbs @actor: Actor
|
|
8
|
+
# @rbs @last_used_at: Float
|
|
9
|
+
# @rbs @pass_exhausted: bool
|
|
10
|
+
|
|
11
|
+
attr_reader :lease, :actor
|
|
12
|
+
|
|
13
|
+
# @rbs (lease: Lease) -> void
|
|
14
|
+
def initialize(lease:)
|
|
15
|
+
@lease = lease
|
|
16
|
+
instance = Instance.find(lease.instance_id)
|
|
17
|
+
@actor_class = SolidObjects.registry.fetch(instance.actor_type)
|
|
18
|
+
@actor = build_actor(instance)
|
|
19
|
+
@last_used_at = monotonic_now
|
|
20
|
+
@pass_exhausted = false
|
|
21
|
+
actor.activate
|
|
22
|
+
SolidObjects.instrument(
|
|
23
|
+
:"activation.started",
|
|
24
|
+
instance_id: instance.id,
|
|
25
|
+
actor_type: instance.actor_type,
|
|
26
|
+
actor_id: instance.actor_id,
|
|
27
|
+
owner_id: lease.owner_id,
|
|
28
|
+
generation: lease.generation
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @rbs () -> Integer
|
|
33
|
+
def drain
|
|
34
|
+
processed_count = 0
|
|
35
|
+
started_at = monotonic_now
|
|
36
|
+
@pass_exhausted = false
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
if processed_count >= SolidObjects.configuration.max_messages_per_activation_pass ||
|
|
40
|
+
monotonic_now - started_at >= SolidObjects.configuration.max_activation_duration
|
|
41
|
+
@pass_exhausted = true
|
|
42
|
+
break
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
message = claim_next_message
|
|
46
|
+
break unless message
|
|
47
|
+
|
|
48
|
+
Executor.new(activation: self, message:).call
|
|
49
|
+
processed_count += 1
|
|
50
|
+
@last_used_at = monotonic_now
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
processed_count
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @rbs () -> bool
|
|
57
|
+
def ready?
|
|
58
|
+
now = SolidObjects.database_adapter.database_now
|
|
59
|
+
ReadyMessage.where(instance_id: lease.instance_id, available_at: ..now).exists? ||
|
|
60
|
+
ClaimedMessage.where(
|
|
61
|
+
instance_id: lease.instance_id,
|
|
62
|
+
process_id: lease.owner_id,
|
|
63
|
+
activation_generation: lease.generation
|
|
64
|
+
).exists?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs () -> bool
|
|
68
|
+
def pass_exhausted?
|
|
69
|
+
@pass_exhausted
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @rbs () -> bool
|
|
73
|
+
def idle?
|
|
74
|
+
monotonic_now - @last_used_at >= SolidObjects.configuration.idle_deactivation_timeout
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs () -> void
|
|
78
|
+
def renew_lease
|
|
79
|
+
@lease = lease.renew
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @rbs () -> bool
|
|
83
|
+
def lease_renewal_due?
|
|
84
|
+
lease.expires_at - SolidObjects.database_adapter.database_now <=
|
|
85
|
+
SolidObjects.configuration.lease_renewal_interval
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @rbs () -> void
|
|
89
|
+
def yield_ready_messages
|
|
90
|
+
now = SolidObjects.database_adapter.database_now
|
|
91
|
+
ReadyMessage
|
|
92
|
+
.where(instance_id: lease.instance_id, available_at: ..now)
|
|
93
|
+
.update_all(available_at: now)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs (Hash[String, untyped]) -> void
|
|
97
|
+
def restore_state(state_data)
|
|
98
|
+
@actor = actor_class.new(
|
|
99
|
+
actor_id: actor.actor_id,
|
|
100
|
+
state: State.new(actor_class.definition.state_definition, state_data)
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs () -> void
|
|
105
|
+
def deactivate
|
|
106
|
+
actor.deactivate
|
|
107
|
+
lease.release
|
|
108
|
+
rescue LostActivation
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
attr_reader :actor_class
|
|
115
|
+
|
|
116
|
+
# @rbs (Instance) -> Actor
|
|
117
|
+
def build_actor(instance)
|
|
118
|
+
state_data = actor_class.definition.migrate_state(instance.state_version, instance.state)
|
|
119
|
+
actor_class.new(
|
|
120
|
+
actor_id: instance.actor_id,
|
|
121
|
+
state: State.new(actor_class.definition.state_definition, state_data)
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @rbs () -> Message?
|
|
126
|
+
def claim_next_message
|
|
127
|
+
lease.fenced_transaction do |instance|
|
|
128
|
+
recover_stale_claim(instance)
|
|
129
|
+
ready_message = ReadyMessage
|
|
130
|
+
.where(instance_id: instance.id)
|
|
131
|
+
.order(:sequence)
|
|
132
|
+
.first
|
|
133
|
+
next unless ready_message
|
|
134
|
+
|
|
135
|
+
now = SolidObjects.database_adapter.database_now
|
|
136
|
+
next if ready_message.available_at > now
|
|
137
|
+
|
|
138
|
+
message = Message.lock.find(ready_message.message_id)
|
|
139
|
+
ready_message.destroy!
|
|
140
|
+
message.update!(
|
|
141
|
+
attempt_count: message.attempt_count + 1,
|
|
142
|
+
started_at: now
|
|
143
|
+
)
|
|
144
|
+
ClaimedMessage.create!(
|
|
145
|
+
message:,
|
|
146
|
+
instance:,
|
|
147
|
+
process_id: lease.owner_id,
|
|
148
|
+
activation_generation: lease.generation,
|
|
149
|
+
claimed_at: now
|
|
150
|
+
)
|
|
151
|
+
message
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (Instance) -> void
|
|
156
|
+
def recover_stale_claim(instance)
|
|
157
|
+
claimed_message = ClaimedMessage
|
|
158
|
+
.joins(:message)
|
|
159
|
+
.where(instance_id: instance.id)
|
|
160
|
+
.order("#{Message.table_name}.sequence")
|
|
161
|
+
.first
|
|
162
|
+
return unless claimed_message
|
|
163
|
+
return if claimed_message.process_id == lease.owner_id &&
|
|
164
|
+
claimed_message.activation_generation == lease.generation
|
|
165
|
+
|
|
166
|
+
message = claimed_message.message
|
|
167
|
+
claimed_message.destroy!
|
|
168
|
+
return if message.completed? || message.dead?
|
|
169
|
+
|
|
170
|
+
ReadyMessage.create!(
|
|
171
|
+
message:,
|
|
172
|
+
instance:,
|
|
173
|
+
sequence: message.sequence,
|
|
174
|
+
available_at: SolidObjects.database_adapter.database_now
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @rbs () -> Float
|
|
179
|
+
def monotonic_now
|
|
180
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|