orbitcast 0.3.1-aarch64-linux-gnu
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/LICENSE.txt +28 -0
- data/README.md +100 -0
- data/app/assets/javascripts/orbitcast/recovery.js +196 -0
- data/db/orbitcast_migrate/001_create_orbitcast_payloads.rb +39 -0
- data/db/orbitcast_migrate/002_create_orbitcast_presence.rb +54 -0
- data/db/orbitcast_migrate/003_create_orbitcast_broker.rb +102 -0
- data/exe/orbitcast +11 -0
- data/lib/action_cable/subscription_adapter/orbitcast.rb +102 -0
- data/lib/generators/orbitcast/install/install_generator.rb +223 -0
- data/lib/generators/orbitcast/install/templates/cable.yml.tt +48 -0
- data/lib/generators/orbitcast/install/templates/orbitcast.rb.tt +156 -0
- data/lib/orbitcast/binary.rb +234 -0
- data/lib/orbitcast/channel.rb +459 -0
- data/lib/orbitcast/channel_registry.rb +131 -0
- data/lib/orbitcast/configuration.rb +636 -0
- data/lib/orbitcast/connection.rb +338 -0
- data/lib/orbitcast/control.rb +72 -0
- data/lib/orbitcast/dispatcher.rb +306 -0
- data/lib/orbitcast/engine.rb +98 -0
- data/lib/orbitcast/error.rb +54 -0
- data/lib/orbitcast/launcher.rb +302 -0
- data/lib/orbitcast/protocol/v1/control_message.rb +134 -0
- data/lib/orbitcast/protocol/v1/effects.rb +172 -0
- data/lib/orbitcast/protocol/v1/reply.rb +268 -0
- data/lib/orbitcast/protocol/v1/request.rb +364 -0
- data/lib/orbitcast/protocol/v1.rb +291 -0
- data/lib/orbitcast/protocol.rb +19 -0
- data/lib/orbitcast/publisher/control.rb +67 -0
- data/lib/orbitcast/publisher/http.rb +180 -0
- data/lib/orbitcast/publisher/instrumentation.rb +47 -0
- data/lib/orbitcast/publisher/postgres.rb +407 -0
- data/lib/orbitcast/rails_endpoint.rb +207 -0
- data/lib/orbitcast/recovery.rb +166 -0
- data/lib/orbitcast/state_codec.rb +186 -0
- data/lib/orbitcast/streams.rb +129 -0
- data/lib/orbitcast/version.rb +10 -0
- data/lib/orbitcast.rb +68 -0
- data/vendor/aarch64-linux-gnu/bin/orbitcast +0 -0
- data/vendor/aarch64-linux-gnu/manifest.json +14 -0
- metadata +130 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_cable"
|
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
require_relative "channel"
|
|
8
|
+
require_relative "channel_registry"
|
|
9
|
+
require_relative "connection"
|
|
10
|
+
require_relative "error"
|
|
11
|
+
require_relative "protocol"
|
|
12
|
+
|
|
13
|
+
module Orbitcast
|
|
14
|
+
# Runs one validated RPC request against the application's connection and
|
|
15
|
+
# channel classes and returns one reply.
|
|
16
|
+
#
|
|
17
|
+
# This is the whole Rails execution model in one place:
|
|
18
|
+
#
|
|
19
|
+
# describe capability handshake; no session, no callbacks
|
|
20
|
+
# identify connect / reject_unauthorized_connection
|
|
21
|
+
# open_channel registry lookup, subscribed, reject
|
|
22
|
+
# close_channel unsubscribed, best effort, effects suppressed
|
|
23
|
+
# run_action perform_action through the action allowlist + filters
|
|
24
|
+
# release_connection remaining unsubscribes + disconnect, best effort
|
|
25
|
+
#
|
|
26
|
+
# Requests are **stateless**: every call rebuilds the connection from the
|
|
27
|
+
# identity and declared state the relay handed back, runs the callbacks, and
|
|
28
|
+
# returns what changed. Nothing is memoized between calls, no database
|
|
29
|
+
# connection is held for a socket's lifetime, and an instance variable that
|
|
30
|
+
# was not declared with `orbitcast_state` does not survive the request.
|
|
31
|
+
class Dispatcher
|
|
32
|
+
V1 = Protocol::V1
|
|
33
|
+
|
|
34
|
+
# Named capabilities reported by `describe`. The relay admits a node by
|
|
35
|
+
# envelope version and capability, never by build equality.
|
|
36
|
+
CAPABILITIES = %w[rails_dispatch].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(config: nil, registry: nil, connection_class: nil, logger: nil)
|
|
39
|
+
@config = config || Orbitcast.config
|
|
40
|
+
@registry = registry
|
|
41
|
+
@connection_class = connection_class
|
|
42
|
+
@logger = logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_reader :config
|
|
46
|
+
|
|
47
|
+
def logger
|
|
48
|
+
@logger ||= (defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger) || null_logger
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def registry
|
|
52
|
+
@registry ||= ChannelRegistry.build(@config.channels)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @param request [Orbitcast::Protocol::V1::Request] already validated
|
|
56
|
+
# @return [Orbitcast::Protocol::V1::Reply]
|
|
57
|
+
def call(request)
|
|
58
|
+
case request.operation
|
|
59
|
+
when V1::OPERATION_DESCRIBE then describe(request)
|
|
60
|
+
when V1::OPERATION_IDENTIFY then identify(request)
|
|
61
|
+
when V1::OPERATION_OPEN_CHANNEL then open_channel(request)
|
|
62
|
+
when V1::OPERATION_CLOSE_CHANNEL then close_channel(request)
|
|
63
|
+
when V1::OPERATION_RUN_ACTION then run_action(request)
|
|
64
|
+
when V1::OPERATION_RELEASE_CONNECTION then release_connection(request)
|
|
65
|
+
else
|
|
66
|
+
# Unreachable: Request.from refuses unknown operations.
|
|
67
|
+
V1::Reply.fault(request, "unknown operation")
|
|
68
|
+
end
|
|
69
|
+
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
|
70
|
+
# An application may refuse from a channel callback too, not just from
|
|
71
|
+
# `connect`. A refusal is a decision, so it is a denial, not a fault.
|
|
72
|
+
V1::Reply.denied(request, "unauthorized")
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
# A Rails-side failure. The relay closes the session with
|
|
75
|
+
# `reconnect: true`; it never falls back to signed-only acceptance.
|
|
76
|
+
log_exception(request, e)
|
|
77
|
+
V1::Reply.fault(request, e.class.name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The connection class the application wired up. Resolved per call so a
|
|
81
|
+
# reloaded class is picked up (the endpoint runs inside the executor).
|
|
82
|
+
def connection_class
|
|
83
|
+
name = @connection_class || config.connection_class
|
|
84
|
+
return name if name.is_a?(Class)
|
|
85
|
+
|
|
86
|
+
constant = name.to_s.safe_constantize
|
|
87
|
+
unless constant.is_a?(Class) && constant <= Orbitcast::Connection
|
|
88
|
+
raise ConfigurationError,
|
|
89
|
+
"#{name} is not an Orbitcast::Connection. In Rails-dispatch mode " \
|
|
90
|
+
"ApplicationCable::Connection must inherit from Orbitcast::Connection " \
|
|
91
|
+
"(config.orbitcast.connection_class selects another class)."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
constant
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Whether this endpoint is ready to serve dispatch traffic.
|
|
98
|
+
def ready?
|
|
99
|
+
connection_class
|
|
100
|
+
!registry.empty?
|
|
101
|
+
rescue StandardError
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# --- operations ---------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def describe(request)
|
|
110
|
+
V1::Reply.describe(
|
|
111
|
+
request,
|
|
112
|
+
supported_versions: [V1::VERSION],
|
|
113
|
+
software_version: Orbitcast::VERSION,
|
|
114
|
+
capabilities: CAPABILITIES,
|
|
115
|
+
ready: ready?
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def identify(request)
|
|
120
|
+
effects = V1::EffectCollector.new
|
|
121
|
+
connection = build_connection(request, effects, restore_identity: false)
|
|
122
|
+
|
|
123
|
+
begin
|
|
124
|
+
connection.orbitcast_connect
|
|
125
|
+
rescue ActionCable::Connection::Authorization::UnauthorizedError
|
|
126
|
+
return V1::Reply.denied(request, "unauthorized")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
V1::Reply.accepted(
|
|
130
|
+
request,
|
|
131
|
+
effects: effects,
|
|
132
|
+
identity: connection.orbitcast_identity,
|
|
133
|
+
state: state_change(request.connection.state, connection.orbitcast_dump_state)
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def open_channel(request)
|
|
138
|
+
effects = V1::EffectCollector.new
|
|
139
|
+
identifier = request.subscription.identifier
|
|
140
|
+
|
|
141
|
+
params = decode_identifier(identifier)
|
|
142
|
+
return V1::Reply.denied(request, "malformed channel identifier") if params.nil?
|
|
143
|
+
|
|
144
|
+
channel_class = registry.resolve(params[:channel])
|
|
145
|
+
if channel_class.nil?
|
|
146
|
+
logger.error { "Orbitcast: refused subscription to #{params[:channel].inspect}" }
|
|
147
|
+
return V1::Reply.denied(request, "channel is not in the Orbitcast channel registry")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
connection = build_connection(request, effects)
|
|
151
|
+
channel = build_channel(channel_class, connection, request)
|
|
152
|
+
connection.subscriptions.add(channel)
|
|
153
|
+
|
|
154
|
+
connection.orbitcast_run_command { channel.subscribe_to_channel }
|
|
155
|
+
|
|
156
|
+
if channel.orbitcast_rejected?
|
|
157
|
+
# Everything the callbacks collected before the refusal is dropped by
|
|
158
|
+
# the reply builder: a denial changes nothing.
|
|
159
|
+
return V1::Reply.denied(request, "subscription rejected")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
V1::Reply.accepted(
|
|
163
|
+
request,
|
|
164
|
+
effects: effects,
|
|
165
|
+
state: state_change(request.connection.state, connection.orbitcast_dump_state),
|
|
166
|
+
subscription_state: state_change(request.subscription.state, channel.orbitcast_dump_state)
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def run_action(request)
|
|
171
|
+
effects = V1::EffectCollector.new
|
|
172
|
+
identifier = request.subscription.identifier
|
|
173
|
+
|
|
174
|
+
params = decode_identifier(identifier)
|
|
175
|
+
return V1::Reply.denied(request, "malformed channel identifier") if params.nil?
|
|
176
|
+
|
|
177
|
+
channel_class = registry.resolve(params[:channel])
|
|
178
|
+
return V1::Reply.denied(request, "channel is not in the Orbitcast channel registry") if channel_class.nil?
|
|
179
|
+
|
|
180
|
+
data = begin
|
|
181
|
+
request.action.data
|
|
182
|
+
rescue JSON::ParserError
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
return V1::Reply.denied(request, "action data is not JSON") unless data.is_a?(Hash)
|
|
186
|
+
|
|
187
|
+
connection = build_connection(request, effects)
|
|
188
|
+
channel = build_channel(channel_class, connection, request)
|
|
189
|
+
connection.subscriptions.add(channel)
|
|
190
|
+
|
|
191
|
+
unless channel.orbitcast_processable_action?(data)
|
|
192
|
+
return V1::Reply.denied(request, "unprocessable action")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
connection.orbitcast_run_command { channel.perform_action(data) }
|
|
196
|
+
|
|
197
|
+
V1::Reply.accepted(
|
|
198
|
+
request,
|
|
199
|
+
effects: effects,
|
|
200
|
+
state: state_change(request.connection.state, connection.orbitcast_dump_state),
|
|
201
|
+
subscription_state: state_change(request.subscription.state, channel.orbitcast_dump_state)
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Post-hoc: the client has already left and the relay has already dropped
|
|
206
|
+
# the membership. Rails cannot veto it, and nothing it returns here can
|
|
207
|
+
# resurrect the subscription, so the collector is suppressed.
|
|
208
|
+
def close_channel(request)
|
|
209
|
+
effects = V1::EffectCollector.new(suppressed: true)
|
|
210
|
+
params = decode_identifier(request.subscription.identifier)
|
|
211
|
+
channel_class = params && registry.resolve(params[:channel])
|
|
212
|
+
|
|
213
|
+
if channel_class
|
|
214
|
+
connection = build_connection(request, effects)
|
|
215
|
+
channel = build_channel(channel_class, connection, request)
|
|
216
|
+
connection.orbitcast_run_command { channel.unsubscribe_from_channel }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
V1::Reply.accepted(request)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Best effort, by definition: on process or network loss this never runs at
|
|
223
|
+
# all. Every subscription is unwound independently so one raising
|
|
224
|
+
# `unsubscribed` cannot swallow the `disconnect` callback.
|
|
225
|
+
def release_connection(request)
|
|
226
|
+
effects = V1::EffectCollector.new(suppressed: true)
|
|
227
|
+
connection = build_connection(request, effects)
|
|
228
|
+
|
|
229
|
+
request.release.subscriptions.each do |snapshot|
|
|
230
|
+
params = decode_identifier(snapshot.identifier)
|
|
231
|
+
channel_class = params && registry.resolve(params[:channel])
|
|
232
|
+
next unless channel_class
|
|
233
|
+
|
|
234
|
+
begin
|
|
235
|
+
channel = build_channel(channel_class, connection, request, subscription: snapshot)
|
|
236
|
+
connection.orbitcast_run_command { channel.unsubscribe_from_channel }
|
|
237
|
+
rescue StandardError => e
|
|
238
|
+
log_exception(request, e)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
connection.orbitcast_disconnect
|
|
243
|
+
|
|
244
|
+
V1::Reply.accepted(request)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# --- construction -------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
def build_connection(request, effects, restore_identity: true)
|
|
250
|
+
connection = connection_class.new(request.connection, effects: effects, logger: logger)
|
|
251
|
+
connection.orbitcast_restore_identity(request.connection.identity) if restore_identity
|
|
252
|
+
connection.orbitcast_restore_state(StateCodec.load_map(request.connection.state))
|
|
253
|
+
connection
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def build_channel(channel_class, connection, request, subscription: nil)
|
|
257
|
+
subscription ||= request.subscription
|
|
258
|
+
params = decode_identifier(subscription.identifier)
|
|
259
|
+
|
|
260
|
+
channel = channel_class.new(connection, subscription.identifier, params)
|
|
261
|
+
channel.orbitcast_installed_streams = subscription.streams
|
|
262
|
+
channel.orbitcast_restore_state(StateCodec.load_map(subscription.state))
|
|
263
|
+
channel
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ActionCable decodes the identifier itself and treats the decoded hash as
|
|
267
|
+
# `params`; so does this, because the identifier is what the relay routes
|
|
268
|
+
# by. The `params` field of the snapshot is a convenience copy and is never
|
|
269
|
+
# authoritative.
|
|
270
|
+
#
|
|
271
|
+
# Plain `JSON.parse`, not `ActiveSupport::JSON.decode`: an identifier is
|
|
272
|
+
# routing data, and no part of it should be coerced into a Time (or
|
|
273
|
+
# anything else) on the way to `params`.
|
|
274
|
+
#
|
|
275
|
+
# @return [ActiveSupport::HashWithIndifferentAccess, nil]
|
|
276
|
+
def decode_identifier(identifier)
|
|
277
|
+
decoded = JSON.parse(identifier)
|
|
278
|
+
decoded.is_a?(Hash) ? decoded.with_indifferent_access : nil
|
|
279
|
+
rescue StandardError
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Absent means unchanged, so a map that did not move is omitted entirely.
|
|
284
|
+
# A map that lost every key is `{}`, which is an explicit clear — the two
|
|
285
|
+
# must never be confused, which is why this compares rather than always
|
|
286
|
+
# emitting.
|
|
287
|
+
def state_change(previous, current)
|
|
288
|
+
previous = {} unless previous.is_a?(Hash)
|
|
289
|
+
return V1::Reply::UNCHANGED if current == previous
|
|
290
|
+
|
|
291
|
+
current
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def log_exception(request, error)
|
|
295
|
+
logger.error do
|
|
296
|
+
"Orbitcast #{request.operation} raised #{error.class}: #{error.message}\n" \
|
|
297
|
+
"#{Array(error.backtrace).first(10).join("\n")}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def null_logger
|
|
302
|
+
require "logger"
|
|
303
|
+
@null_logger ||= ::Logger.new(IO::NULL)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module Orbitcast
|
|
6
|
+
# Minimal Rails engine.
|
|
7
|
+
#
|
|
8
|
+
# It exists for two reasons only: to expose `config.orbitcast.*` and to carry
|
|
9
|
+
# the installer generator and the migration path. It has no `app/` tree, adds
|
|
10
|
+
# nothing to the autoload or eager-load paths, and mounts no routes in
|
|
11
|
+
# signed mode.
|
|
12
|
+
#
|
|
13
|
+
# It deliberately does NOT start the relay. The Rust process is separately
|
|
14
|
+
# supervised (`bundle exec orbitcast serve` under bin/dev or the production
|
|
15
|
+
# process manager — Puma, shuttlebay, or otherwise); launching it from an
|
|
16
|
+
# initializer would spawn one relay per app-server worker, per console, and
|
|
17
|
+
# per rake task, each fighting over the same listener socket.
|
|
18
|
+
class Engine < ::Rails::Engine
|
|
19
|
+
isolate_namespace Orbitcast
|
|
20
|
+
|
|
21
|
+
# Registers `config.orbitcast` in the shared railtie options so an
|
|
22
|
+
# application can write `config.orbitcast.mode = :signed` in
|
|
23
|
+
# config/environments/*.rb.
|
|
24
|
+
config.orbitcast = ActiveSupport::OrderedOptions.new
|
|
25
|
+
|
|
26
|
+
# No autoloading surprises: nothing in this gem's lib/ is autoloaded, and
|
|
27
|
+
# the engine contributes no paths of its own.
|
|
28
|
+
config.autoload_once_paths = []
|
|
29
|
+
config.eager_load_paths = []
|
|
30
|
+
|
|
31
|
+
initializer "orbitcast.configuration", before: :load_config_initializers do |app|
|
|
32
|
+
options = app.config.orbitcast
|
|
33
|
+
# Unknown keys raise here, at boot, rather than silently disabling a
|
|
34
|
+
# security-relevant setting at runtime.
|
|
35
|
+
Orbitcast.configure { |config| config.assign(options.to_h) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The installer copies Orbitcast's migrations into the application's own
|
|
39
|
+
# `db/orbitcast_migrate/` (never into `db/migrate`, so they are
|
|
40
|
+
# recognisable and removable). When that directory exists it is appended
|
|
41
|
+
# to the application's migration paths, which is what `bin/rails
|
|
42
|
+
# db:migrate` and `ActiveRecord::Tasks::DatabaseTasks.migrations_paths`
|
|
43
|
+
# read. Nothing runs DDL at boot.
|
|
44
|
+
initializer "orbitcast.migrations" do |app|
|
|
45
|
+
migrate = app.root.join("db", "orbitcast_migrate")
|
|
46
|
+
next unless migrate.directory?
|
|
47
|
+
|
|
48
|
+
paths = app.config.paths["db/migrate"]
|
|
49
|
+
paths << migrate.to_s unless paths.expanded.include?(migrate.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The derived Orbitcast stream key: when no
|
|
53
|
+
# `streams_secret` is configured explicitly, `Orbitcast::Configuration`
|
|
54
|
+
# derives one from the application's key generator under the purpose
|
|
55
|
+
# `orbitcast/streams/v1/<namespace>`. Only the derived 32 bytes ever leave
|
|
56
|
+
# this process; `secret_key_base` and the master key do not. The namespace
|
|
57
|
+
# may still arrive later (cable.yml is read lazily by the adapter and the
|
|
58
|
+
# launcher), which is why the generator is handed over and the key is
|
|
59
|
+
# derived on first use rather than here.
|
|
60
|
+
config.after_initialize do |app|
|
|
61
|
+
Orbitcast.config.key_generator ||= app.key_generator
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Rails-dispatch mode only: mount the private RPC endpoint the relay calls
|
|
65
|
+
# back into. Signed-mode applications mount nothing.
|
|
66
|
+
#
|
|
67
|
+
# ##########################################################################
|
|
68
|
+
# The mounted path speaks for every user of this application: it runs
|
|
69
|
+
# `connect`, channel callbacks, and actions on behalf of whoever the relay
|
|
70
|
+
# says is connected. It authenticates the relay's bearer credential itself,
|
|
71
|
+
# but it must ALSO be blocked at the public proxy / load balancer:
|
|
72
|
+
#
|
|
73
|
+
# # nginx
|
|
74
|
+
# location /_orbitcast/ { deny all; }
|
|
75
|
+
#
|
|
76
|
+
# # Traefik / Kamal
|
|
77
|
+
# middlewares: [ orbitcast-internal-only ]
|
|
78
|
+
#
|
|
79
|
+
# Reach it from the relay over the private network or over TLS with a
|
|
80
|
+
# rotating credential, never from the internet.
|
|
81
|
+
# ##########################################################################
|
|
82
|
+
initializer "orbitcast.rails_endpoint", after: :load_config_initializers do |app|
|
|
83
|
+
next unless Orbitcast.config.dispatch?
|
|
84
|
+
|
|
85
|
+
require "orbitcast/rails_endpoint"
|
|
86
|
+
|
|
87
|
+
mount_path = Orbitcast.config.rpc_mount_path
|
|
88
|
+
app.routes.append do
|
|
89
|
+
mount Orbitcast::RailsEndpoint.new, at: mount_path, as: :orbitcast_rails_endpoint
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Rails.logger&.info(
|
|
93
|
+
"[orbitcast] Rails-dispatch endpoint mounted at #{mount_path}; block this path at the " \
|
|
94
|
+
"public proxy."
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Orbitcast
|
|
4
|
+
# Base class for every Orbitcast-raised error.
|
|
5
|
+
#
|
|
6
|
+
# All error classes live in this one file on purpose: they are needed by files
|
|
7
|
+
# that must load without the rest of the gem (see `orbitcast/binary.rb`, which
|
|
8
|
+
# the executable wrapper loads before Rails exists). Zeitwerk only requires
|
|
9
|
+
# `Orbitcast::Error` to be defined here; the siblings are plain extra constants
|
|
10
|
+
# and are never expected at their own paths.
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Raised for unknown keys or invalid values in Orbitcast configuration.
|
|
14
|
+
# Inherits ArgumentError so callers may rescue either.
|
|
15
|
+
class ConfigurationError < ArgumentError; end
|
|
16
|
+
|
|
17
|
+
# Raised when the resolved Orbitcast executable is unusable: it cannot be
|
|
18
|
+
# found, or an `ORBITCAST_BINARY` override fails the `doctor --json` check
|
|
19
|
+
# (wrong version, missing feature, unsupported envelope version).
|
|
20
|
+
class BinaryError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when no usable Orbitcast executable can be resolved.
|
|
23
|
+
class BinaryNotFoundError < BinaryError; end
|
|
24
|
+
|
|
25
|
+
# Raised when an encoded notice exceeds PostgreSQL's NOTIFY payload limit.
|
|
26
|
+
class PayloadTooLarge < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when the publisher pool cannot hand out a connection in time.
|
|
29
|
+
class PoolTimeout < Error; end
|
|
30
|
+
|
|
31
|
+
# Raised when the overflow relay is enabled but `orbitcast_payloads` does not
|
|
32
|
+
# exist. Orbitcast never creates it implicitly: install the migration.
|
|
33
|
+
class OverflowTableMissing < Error; end
|
|
34
|
+
|
|
35
|
+
# Raised when a v1 envelope is not usable: unsupported version, unknown
|
|
36
|
+
# tagged value, missing payload, or a value outside the documented limits.
|
|
37
|
+
# The `field` is the envelope field that failed, for logs and tests.
|
|
38
|
+
class ProtocolError < Error
|
|
39
|
+
attr_reader :field
|
|
40
|
+
|
|
41
|
+
def initialize(message, field: nil)
|
|
42
|
+
super(message)
|
|
43
|
+
@field = field
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when the application asks for a channel construct the relay cannot
|
|
48
|
+
# run (a stream transformation block, a periodic timer). Unsupported uses
|
|
49
|
+
# are detected and fail clearly instead of pretending.
|
|
50
|
+
class UnsupportedChannelFeature < Error; end
|
|
51
|
+
|
|
52
|
+
# Raised when a private HTTP publication ingress call fails.
|
|
53
|
+
class IngressError < Error; end
|
|
54
|
+
end
|