pgbus 0.17.2 → 0.18.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +55 -9
- data/Rakefile +26 -0
- data/app/assets/javascripts/pgbus/stream_source_element.js +37 -4
- data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
- data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
- data/app/controllers/pgbus/frontends_controller.rb +25 -11
- data/app/frontend/pgbus/application.min.js +2 -0
- data/app/frontend/pgbus/application.min.js.map +1 -0
- data/app/frontend/pgbus/modules/charts.js +24 -6
- data/app/frontend/pgbus/modules/charts.min.js +2 -0
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
- data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
- data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
- data/app/views/layouts/pgbus/application.html.erb +0 -1
- data/config/routes.rb +1 -1
- data/lib/generators/pgbus/add_presence_generator.rb +2 -1
- data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
- data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
- data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
- data/lib/pgbus/configuration/presence_settings.rb +103 -0
- data/lib/pgbus/configuration/stream_authorization.rb +36 -0
- data/lib/pgbus/configuration.rb +16 -1
- data/lib/pgbus/engine.rb +13 -7
- data/lib/pgbus/generators/migration_detector.rb +9 -1
- data/lib/pgbus/streams/envelope.rb +3 -2
- data/lib/pgbus/streams/presence.rb +317 -81
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/stream_app.rb +43 -14
- data/lib/pgbus/web/streamer/connection.rb +26 -9
- data/lib/pgbus/web/streamer/falcon_connection.rb +23 -9
- data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
- data/lib/pgbus/web/streamer/instance.rb +25 -8
- data/lib/pgbus/web/streamer/peer_close_watcher.rb +183 -0
- data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +26 -39
- data/lib/pgbus.rb +2 -0
- metadata +15 -1
|
@@ -86,7 +86,6 @@
|
|
|
86
86
|
<%# Importmap for ES modules — keys are internal symbols, values are app-controlled paths, no user input %>
|
|
87
87
|
<% importmaps = Pgbus::FrontendsController.js_modules.keys.index_with { |mod| frontend_module_path(mod, format: :js, locale: nil) } %>
|
|
88
88
|
<%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %> <%# herb:disable erb-no-unsafe-raw %>
|
|
89
|
-
<%= tag.script("", src: frontend_static_path(:apexcharts, format: :js, locale: nil), nonce: content_security_policy_nonce) %>
|
|
90
89
|
|
|
91
90
|
<script type="module" nonce="<%= content_security_policy_nonce %>">
|
|
92
91
|
import "application";
|
data/config/routes.rb
CHANGED
|
@@ -99,7 +99,7 @@ Pgbus::Engine.routes.draw do
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
scope :frontend, controller: :frontends, defaults: { version: Pgbus::VERSION.tr(".", "-") } do
|
|
102
|
-
get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format:
|
|
102
|
+
get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: /js|map/ }
|
|
103
103
|
get "static/:version/:id", action: :static, as: :frontend_static
|
|
104
104
|
end
|
|
105
105
|
end
|
|
@@ -33,7 +33,8 @@ module Pgbus
|
|
|
33
33
|
say " 2. Use in your code:"
|
|
34
34
|
say " Pgbus.stream(@room).presence.join(member_id: current_user.id.to_s)"
|
|
35
35
|
say " Pgbus.stream(@room).presence.members"
|
|
36
|
-
say " 3.
|
|
36
|
+
say " 3. Connection-driven presence (streams_presence_patterns) expires silent"
|
|
37
|
+
say " seats on its own. Rows you join by member_id need a periodic sweeper:"
|
|
37
38
|
say " Pgbus.stream(@room).presence.sweep!(older_than: 60.seconds.ago)"
|
|
38
39
|
say ""
|
|
39
40
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
require_relative "migration_path"
|
|
6
|
+
|
|
7
|
+
module Pgbus
|
|
8
|
+
module Generators
|
|
9
|
+
class AddPresenceSeatsGenerator < Rails::Generators::Base
|
|
10
|
+
include ActiveRecord::Generators::Migration
|
|
11
|
+
include MigrationPath
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
desc "Add per-connection seats (connection_id, seat, state) to pgbus_presence_members"
|
|
16
|
+
|
|
17
|
+
class_option :database,
|
|
18
|
+
type: :string,
|
|
19
|
+
default: nil,
|
|
20
|
+
desc: "Use a separate database for pgbus tables (e.g. --database=pgbus)"
|
|
21
|
+
|
|
22
|
+
def create_migration_file
|
|
23
|
+
migration_template "add_presence_seats.rb.erb",
|
|
24
|
+
File.join(pgbus_migrate_path, "add_pgbus_presence_seats.rb")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def display_post_install
|
|
28
|
+
say ""
|
|
29
|
+
say "Pgbus presence seats installed!", :green
|
|
30
|
+
say ""
|
|
31
|
+
say "Next steps:"
|
|
32
|
+
say " 1. Run: rails db:migrate#{migrate_command_suffix}"
|
|
33
|
+
say " 2. Restart the app: connection-driven presence (streams_presence_patterns)"
|
|
34
|
+
say " then keeps one row per SSE connection, expired after streams_presence_ttl"
|
|
35
|
+
say ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def migration_version
|
|
41
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -4,23 +4,48 @@ class AddPgbusPresence < ActiveRecord::Migration<%= migration_version %>
|
|
|
4
4
|
t.string :stream_name, null: false
|
|
5
5
|
t.string :member_id, null: false
|
|
6
6
|
t.jsonb :metadata, null: false, default: {}
|
|
7
|
+
# One row per SSE connection for connection-driven presence (seats).
|
|
8
|
+
# NULL for rows written by member-keyed join/leave calls.
|
|
9
|
+
t.string :connection_id
|
|
10
|
+
# The client's per-tab id (sent by <pgbus-stream-source presence> as ?seat=).
|
|
11
|
+
t.string :seat
|
|
12
|
+
# Ephemeral per-seat state, written by Presence#set_state.
|
|
13
|
+
t.jsonb :state, null: false, default: {}
|
|
7
14
|
t.datetime :joined_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
8
15
|
t.datetime :last_seen_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
|
|
9
16
|
end
|
|
10
17
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# Pgbus::Streams::Presence#join.
|
|
18
|
+
# A member-keyed row (no connection) appears once per stream. Upsert
|
|
19
|
+
# target of Pgbus::Streams::Presence#join(member_id:).
|
|
14
20
|
add_index :pgbus_presence_members,
|
|
15
21
|
%i[stream_name member_id],
|
|
16
22
|
unique: true,
|
|
17
|
-
|
|
23
|
+
where: "connection_id IS NULL",
|
|
24
|
+
name: "idx_pgbus_presence_members_member_key"
|
|
18
25
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
26
|
+
# A seat row appears once per connection. Upsert target of
|
|
27
|
+
# Pgbus::Streams::Presence#join(connection_id:).
|
|
28
|
+
add_index :pgbus_presence_members,
|
|
29
|
+
%i[stream_name connection_id],
|
|
30
|
+
unique: true,
|
|
31
|
+
where: "connection_id IS NOT NULL",
|
|
32
|
+
name: "idx_pgbus_presence_members_connection_key"
|
|
33
|
+
|
|
34
|
+
# All rows of one member (several tabs) on a stream.
|
|
35
|
+
add_index :pgbus_presence_members,
|
|
36
|
+
%i[stream_name member_id],
|
|
37
|
+
name: "idx_pgbus_presence_members_member"
|
|
38
|
+
|
|
39
|
+
# Sweeping by last_seen_at expires stale members:
|
|
40
|
+
# WHERE stream_name = ? AND last_seen_at < ?
|
|
22
41
|
add_index :pgbus_presence_members,
|
|
23
42
|
%i[stream_name last_seen_at],
|
|
24
43
|
name: "idx_pgbus_presence_members_sweep"
|
|
44
|
+
|
|
45
|
+
# The streamer heartbeat expires silent seats across every stream.
|
|
46
|
+
add_index :pgbus_presence_members,
|
|
47
|
+
:last_seen_at,
|
|
48
|
+
where: "connection_id IS NOT NULL",
|
|
49
|
+
name: "idx_pgbus_presence_members_seat_expiry"
|
|
25
50
|
end
|
|
26
51
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class AddPgbusPresenceSeats < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
add_column :pgbus_presence_members, :connection_id, :string
|
|
4
|
+
add_column :pgbus_presence_members, :seat, :string
|
|
5
|
+
add_column :pgbus_presence_members, :state, :jsonb, null: false, default: {}
|
|
6
|
+
|
|
7
|
+
# Existing rows are member-keyed (connection_id IS NULL) and keep their
|
|
8
|
+
# one-row-per-member key; seat rows get one row per connection.
|
|
9
|
+
remove_index :pgbus_presence_members, name: "idx_pgbus_presence_members_pk"
|
|
10
|
+
add_index :pgbus_presence_members, %i[stream_name member_id],
|
|
11
|
+
unique: true, where: "connection_id IS NULL",
|
|
12
|
+
name: "idx_pgbus_presence_members_member_key"
|
|
13
|
+
add_index :pgbus_presence_members, %i[stream_name connection_id],
|
|
14
|
+
unique: true, where: "connection_id IS NOT NULL",
|
|
15
|
+
name: "idx_pgbus_presence_members_connection_key"
|
|
16
|
+
add_index :pgbus_presence_members, %i[stream_name member_id],
|
|
17
|
+
name: "idx_pgbus_presence_members_member"
|
|
18
|
+
add_index :pgbus_presence_members, :last_seen_at,
|
|
19
|
+
where: "connection_id IS NOT NULL",
|
|
20
|
+
name: "idx_pgbus_presence_members_seat_expiry"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def down
|
|
24
|
+
# Seat rows cannot satisfy the old one-row-per-member key.
|
|
25
|
+
execute "DELETE FROM pgbus_presence_members WHERE connection_id IS NOT NULL"
|
|
26
|
+
|
|
27
|
+
remove_index :pgbus_presence_members, name: "idx_pgbus_presence_members_seat_expiry"
|
|
28
|
+
remove_index :pgbus_presence_members, name: "idx_pgbus_presence_members_member"
|
|
29
|
+
remove_index :pgbus_presence_members, name: "idx_pgbus_presence_members_connection_key"
|
|
30
|
+
remove_index :pgbus_presence_members, name: "idx_pgbus_presence_members_member_key"
|
|
31
|
+
add_index :pgbus_presence_members, %i[stream_name member_id],
|
|
32
|
+
unique: true, name: "idx_pgbus_presence_members_pk"
|
|
33
|
+
|
|
34
|
+
remove_column :pgbus_presence_members, :state
|
|
35
|
+
remove_column :pgbus_presence_members, :seat
|
|
36
|
+
remove_column :pgbus_presence_members, :connection_id
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Configuration
|
|
5
|
+
# Seat expiry and the change hook for connection-driven presence
|
|
6
|
+
# (issue #515). EXPERIMENTAL, like the other streams_presence_* settings.
|
|
7
|
+
#
|
|
8
|
+
# config.streams_presence_ttl = 60 # every stream
|
|
9
|
+
# config.streams_presence_ttl = { /^doc:/ => 5.minutes, default: 60 } # per pattern
|
|
10
|
+
# config.streams_presence_on_change = ->(stream_name, change) { ... }
|
|
11
|
+
#
|
|
12
|
+
# `change` is { type: :join | :leave, connection_id:, seat:, member_id: }.
|
|
13
|
+
# An expired seat is reported as a :leave.
|
|
14
|
+
module PresenceSettings
|
|
15
|
+
DEFAULT_PRESENCE_TTL = 60
|
|
16
|
+
|
|
17
|
+
attr_accessor :streams_presence_on_change
|
|
18
|
+
attr_writer :streams_presence_ttl
|
|
19
|
+
|
|
20
|
+
def streams_presence_ttl
|
|
21
|
+
@streams_presence_ttl.nil? ? DEFAULT_PRESENCE_TTL : @streams_presence_ttl
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Seconds a seat may stay silent on `stream_name` before the heartbeat
|
|
25
|
+
# expires it. A pattern matches like streams_presence_patterns (exact
|
|
26
|
+
# String or Regexp); the first match wins.
|
|
27
|
+
def presence_ttl_for(stream_name)
|
|
28
|
+
ttl = streams_presence_ttl
|
|
29
|
+
return presence_seconds(ttl) unless ttl.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
match = ttl.find { |pattern, _| pattern_matches?(pattern, stream_name) }
|
|
32
|
+
presence_seconds(match ? match.last : ttl.fetch(:default, DEFAULT_PRESENCE_TTL))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The smallest ttl configured: the expiry scan's candidate cutoff.
|
|
36
|
+
def presence_min_ttl
|
|
37
|
+
ttl = streams_presence_ttl
|
|
38
|
+
return presence_seconds(ttl) unless ttl.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
[*ttl.values, ttl.fetch(:default, DEFAULT_PRESENCE_TTL)].map { |v| presence_seconds(v) }.min
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reports one join, leave or expiry to streams_presence_on_change.
|
|
44
|
+
# A raising hook is logged and swallowed, like the rest of the
|
|
45
|
+
# presence path: it must not break a connection or a sweep.
|
|
46
|
+
def notify_presence_change(stream_name, change)
|
|
47
|
+
hook = streams_presence_on_change
|
|
48
|
+
return unless hook
|
|
49
|
+
|
|
50
|
+
hook.call(stream_name, change)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Pgbus.logger.error do
|
|
53
|
+
"[Pgbus] streams_presence_on_change raised for #{stream_name}: #{e.class}: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def validate_presence_settings!
|
|
60
|
+
if !streams_presence_on_change.nil? && !streams_presence_on_change.respond_to?(:call)
|
|
61
|
+
raise Pgbus::ConfigurationError,
|
|
62
|
+
"streams_presence_on_change must respond to #call (a Proc/lambda) or be nil"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
ttl = streams_presence_ttl
|
|
66
|
+
values = ttl.is_a?(Hash) ? ttl.values : [ttl]
|
|
67
|
+
unless values.all? { |v| v.is_a?(Numeric) && v.to_f.positive? }
|
|
68
|
+
raise Pgbus::ConfigurationError,
|
|
69
|
+
"streams_presence_ttl must be a positive number of seconds or a Hash of pattern => seconds"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
warn_presence_ttl_below_heartbeat
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A ttl at or below the heartbeat interval lets another process expire
|
|
76
|
+
# live seats between touches, so every tick reports a leave and a join.
|
|
77
|
+
def warn_presence_ttl_below_heartbeat
|
|
78
|
+
heartbeat = streams_heartbeat_interval
|
|
79
|
+
return unless heartbeat.is_a?(Numeric) && presence_min_ttl <= heartbeat
|
|
80
|
+
|
|
81
|
+
Pgbus.logger.warn do
|
|
82
|
+
"[Pgbus] streams_presence_ttl (#{presence_min_ttl}s) should exceed streams_heartbeat_interval " \
|
|
83
|
+
"(#{heartbeat}s), or live presence seats are expired and re-joined every tick"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Seconds as an Integer when whole, a Float otherwise (a Duration
|
|
88
|
+
# becomes plain seconds), so fractional ttls are not truncated.
|
|
89
|
+
def presence_seconds(value)
|
|
90
|
+
seconds = value.to_f
|
|
91
|
+
seconds == seconds.floor ? seconds.to_i : seconds
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def pattern_matches?(pattern, stream_name)
|
|
95
|
+
case pattern
|
|
96
|
+
when Regexp then pattern.match?(stream_name)
|
|
97
|
+
when :default then false
|
|
98
|
+
else pattern.to_s == stream_name
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pgbus
|
|
4
|
+
class Configuration
|
|
5
|
+
# The authorize hook the engine-mounted StreamApp uses (issue #522). Same
|
|
6
|
+
# contract as StreamApp.new(authorize:):
|
|
7
|
+
#
|
|
8
|
+
# config.streams_authorize = ->(env, stream_name) {
|
|
9
|
+
# user = current_user_from(env)
|
|
10
|
+
# user if user && may_read?(user, stream_name)
|
|
11
|
+
# }
|
|
12
|
+
#
|
|
13
|
+
# A falsy return answers 403, `true` authorizes with no context, any other
|
|
14
|
+
# value becomes the connection context (what visible_to: filters and
|
|
15
|
+
# connection-driven presence read). nil (the default) authorizes every
|
|
16
|
+
# connection with no context. An app that mounts its own
|
|
17
|
+
# StreamApp.new(authorize:) keeps its hook; this one is not consulted.
|
|
18
|
+
#
|
|
19
|
+
# Authorize per stream: the signed name proves pgbus minted it, not that
|
|
20
|
+
# this user may read it (a removed member keeps a working name until the
|
|
21
|
+
# secret rotates). A non-callable value fails validate!; with
|
|
22
|
+
# eager_validation = false every stream request fails closed with a 500.
|
|
23
|
+
module StreamAuthorization
|
|
24
|
+
attr_accessor :streams_authorize
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_stream_authorization!
|
|
29
|
+
return if streams_authorize.nil? || streams_authorize.respond_to?(:call)
|
|
30
|
+
|
|
31
|
+
raise Pgbus::ConfigurationError,
|
|
32
|
+
"streams_authorize must respond to #call (a ->(env, stream_name) { } lambda) or be nil"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/pgbus/configuration.rb
CHANGED
|
@@ -4,6 +4,9 @@ require "logger"
|
|
|
4
4
|
|
|
5
5
|
module Pgbus
|
|
6
6
|
class Configuration
|
|
7
|
+
include PresenceSettings
|
|
8
|
+
include StreamAuthorization
|
|
9
|
+
|
|
7
10
|
# Connection settings
|
|
8
11
|
attr_accessor :database_url, :connection_params, :pool_size, :pool_timeout
|
|
9
12
|
|
|
@@ -215,7 +218,7 @@ module Pgbus
|
|
|
215
218
|
|
|
216
219
|
# Streams (turbo-rails replacement, SSE-based)
|
|
217
220
|
attr_accessor :streams_enabled, :streams_path, :streams_signed_name_secret,
|
|
218
|
-
:streams_default_retention, :streams_retention, :streams_heartbeat_interval,
|
|
221
|
+
:streams_default_retention, :streams_retention, :streams_heartbeat_interval, :streams_peer_close_interval,
|
|
219
222
|
:streams_max_connections, :streams_idle_timeout, :streams_listen_health_check_ms,
|
|
220
223
|
:streams_write_deadline_ms, :streams_falcon_streaming_body,
|
|
221
224
|
:streams_stats_enabled, :streams_test_mode,
|
|
@@ -459,6 +462,10 @@ module Pgbus
|
|
|
459
462
|
@streams_default_retention = 5 * 60 # 5 minutes
|
|
460
463
|
@streams_retention = {}
|
|
461
464
|
@streams_heartbeat_interval = 15
|
|
465
|
+
# Seconds a new SSE connection waits to join the peer-close watch; a
|
|
466
|
+
# client that closes a watched socket is noticed at once (issue #519).
|
|
467
|
+
# nil turns the watcher off and leaves disconnects to the heartbeat.
|
|
468
|
+
@streams_peer_close_interval = 1.0
|
|
462
469
|
@streams_max_connections = 2_000
|
|
463
470
|
@streams_idle_timeout = 3_600 # 1 hour
|
|
464
471
|
# 250ms — this value plays two roles: (1) the TCP keepalive
|
|
@@ -1026,6 +1033,11 @@ module Pgbus
|
|
|
1026
1033
|
raise Pgbus::ConfigurationError, "streams_heartbeat_interval must be a positive number"
|
|
1027
1034
|
end
|
|
1028
1035
|
|
|
1036
|
+
unless streams_peer_close_interval.nil? ||
|
|
1037
|
+
(streams_peer_close_interval.is_a?(Numeric) && streams_peer_close_interval.positive?)
|
|
1038
|
+
raise Pgbus::ConfigurationError, "streams_peer_close_interval must be nil or a positive number"
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1029
1041
|
unless streams_idle_timeout.is_a?(Numeric) && streams_idle_timeout.positive?
|
|
1030
1042
|
raise Pgbus::ConfigurationError, "streams_idle_timeout must be a positive number"
|
|
1031
1043
|
end
|
|
@@ -1093,6 +1105,9 @@ module Pgbus
|
|
|
1093
1105
|
raise Pgbus::ConfigurationError, "streams_presence_member must respond to #call (a Proc/lambda) or be nil"
|
|
1094
1106
|
end
|
|
1095
1107
|
|
|
1108
|
+
validate_presence_settings!
|
|
1109
|
+
validate_stream_authorization!
|
|
1110
|
+
|
|
1096
1111
|
return if streams_orphan_threshold.nil?
|
|
1097
1112
|
return if streams_orphan_threshold.is_a?(Numeric) && streams_orphan_threshold.positive?
|
|
1098
1113
|
|
data/lib/pgbus/engine.rb
CHANGED
|
@@ -139,14 +139,20 @@ module Pgbus
|
|
|
139
139
|
app.middleware.use Pgbus::Streams::WatermarkCacheMiddleware if Pgbus.configuration.streams_enabled
|
|
140
140
|
end
|
|
141
141
|
|
|
142
|
-
# Make
|
|
143
|
-
# pipeline (Propshaft or Sprockets)
|
|
144
|
-
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
142
|
+
# Make the <pgbus-stream-source> element available to the host app's
|
|
143
|
+
# asset pipeline (Propshaft or Sprockets). The importmap pin points at
|
|
144
|
+
# the minified build (rake frontend:js). Propshaft rewrites its
|
|
145
|
+
# sourceMappingURL to the digested .min.js.map; Sprockets serves the
|
|
146
|
+
# comment as written. The readable ES module source stays precompiled for
|
|
147
|
+
# apps that pin or bundle it themselves. When importmap-rails is loaded,
|
|
148
|
+
# auto-pin it so host apps get it without manual configuration.
|
|
147
149
|
initializer "pgbus.streams.assets" do |app|
|
|
148
150
|
if Pgbus.configuration.streams_enabled && app.config.respond_to?(:assets)
|
|
149
|
-
app.config.assets.precompile += %w[
|
|
151
|
+
app.config.assets.precompile += %w[
|
|
152
|
+
pgbus/stream_source_element.min.js
|
|
153
|
+
pgbus/stream_source_element.min.js.map
|
|
154
|
+
pgbus/stream_source_element.js
|
|
155
|
+
]
|
|
150
156
|
end
|
|
151
157
|
end
|
|
152
158
|
|
|
@@ -156,7 +162,7 @@ module Pgbus
|
|
|
156
162
|
next unless Rails.application.respond_to?(:importmap)
|
|
157
163
|
|
|
158
164
|
Rails.application.importmap.pin(
|
|
159
|
-
"pgbus/stream_source_element", to: "pgbus/stream_source_element.js"
|
|
165
|
+
"pgbus/stream_source_element", to: "pgbus/stream_source_element.min.js"
|
|
160
166
|
)
|
|
161
167
|
end
|
|
162
168
|
end
|
|
@@ -73,6 +73,7 @@ module Pgbus
|
|
|
73
73
|
add_stream_stats: "pgbus:add_stream_stats",
|
|
74
74
|
add_stream_queues: "pgbus:add_stream_queues",
|
|
75
75
|
add_presence: "pgbus:add_presence",
|
|
76
|
+
add_presence_seats: "pgbus:add_presence_seats",
|
|
76
77
|
add_queue_states: "pgbus:add_queue_states",
|
|
77
78
|
add_outbox: "pgbus:add_outbox",
|
|
78
79
|
add_recurring: "pgbus:add_recurring",
|
|
@@ -94,6 +95,7 @@ module Pgbus
|
|
|
94
95
|
add_stream_stats: "stream stats table (opt-in real-time Insights)",
|
|
95
96
|
add_stream_queues: "stream queue registry (per-stream retention, orphan sweep, wildcard-worker safety)",
|
|
96
97
|
add_presence: "presence members table (Turbo Streams presence)",
|
|
98
|
+
add_presence_seats: "presence seats (one row per SSE connection, per-seat state)",
|
|
97
99
|
add_queue_states: "queue states table (pause/resume)",
|
|
98
100
|
add_outbox: "outbox entries table (transactional outbox)",
|
|
99
101
|
add_recurring: "recurring tasks + executions tables",
|
|
@@ -182,8 +184,14 @@ module Pgbus
|
|
|
182
184
|
table_exists?("pgbus_stream_queues") ? [] : [:add_stream_queues]
|
|
183
185
|
end
|
|
184
186
|
|
|
187
|
+
# The add_presence template already carries the seat columns, so a
|
|
188
|
+
# missing table needs only that; an existing table without
|
|
189
|
+
# connection_id predates seats (issue #515).
|
|
185
190
|
def presence_migrations
|
|
186
|
-
table_exists?("pgbus_presence_members")
|
|
191
|
+
return [:add_presence] unless table_exists?("pgbus_presence_members")
|
|
192
|
+
return [] if column_names("pgbus_presence_members").include?("connection_id")
|
|
193
|
+
|
|
194
|
+
[:add_presence_seats]
|
|
187
195
|
end
|
|
188
196
|
|
|
189
197
|
def queue_states_migrations
|
|
@@ -49,8 +49,9 @@ module Pgbus
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Emits a `pgbus:connected` frame carrying the server-minted
|
|
52
|
-
# connection id as JSON. Sent once
|
|
53
|
-
#
|
|
52
|
+
# connection id as JSON. Sent once by the streamer's Dispatcher, after
|
|
53
|
+
# the connection is registered and its presence seat joined (issue
|
|
54
|
+
# #521), so the page can read its own connection id and send it back as
|
|
54
55
|
# `X-Pgbus-Connection` on action requests (actor-echo suppression,
|
|
55
56
|
# issue #165). Deliberately omits an `id:` line: this is connection
|
|
56
57
|
# metadata, not a broadcast, and giving it a cursor id would corrupt
|