studio-engine 0.9.0 → 0.10.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 +21 -0
- data/app/models/concerns/studio/broadcastable.rb +40 -0
- data/lib/studio/cable.rb +35 -0
- data/lib/studio/redis.rb +41 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +2 -0
- data/studio-engine.gemspec +5 -0
- metadata +34 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20b0e58f17421ce741452b329000bc657f0dba0304198d7dc1fb59cb448bfc6d
|
|
4
|
+
data.tar.gz: d4446a5bad4b0b744e54ad7d4f275cf77455a68f2cf3289d6d9c472ca1f3c548
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b15b1f884815e8afbad3d9aa181589230193942970d62457075d08c47521c967a005025560e665b436ceb8a6c78f425688430f122cad9a7f21e8a4432bd4ca22
|
|
7
|
+
data.tar.gz: 5a6c6817018309298bb3a8eb686f61d7e93d9718d5b8ce5b5a8e642efc82f99695d6bf0c4f23b9421fa8c65481722e9164934c7cbe47f254e3abb482d7ea42ed
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.10.0 — 2026-06-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Shared websocket / Redis primitive** (`docs/CABLE.md`) — one place for the
|
|
11
|
+
setup every host app's realtime needs, extracted after a SEV-1 (a host app
|
|
12
|
+
shipped an ActionCable channel with no `redis` gem and no TLS cable config; the
|
|
13
|
+
broadcast raised `Gem::LoadError` — a `ScriptError`, not a `StandardError` — which
|
|
14
|
+
escaped a `rescue StandardError` and 500'd every task write).
|
|
15
|
+
- **`redis` is now an engine dependency** (plus `turbo-rails`), so a consuming app
|
|
16
|
+
can never hit that `Gem::LoadError` again.
|
|
17
|
+
- **`Studio::Redis`** — the single source of Redis connection truth for `cable.yml`,
|
|
18
|
+
the `cache_store`, and Sidekiq: `.url`, `.tls?`, and `.options(**extra)`, which
|
|
19
|
+
auto-applies `ssl_params: { verify_mode: VERIFY_NONE }` for Heroku's `rediss://`
|
|
20
|
+
self-signed TLS (the gotcha that silently drops every broadcast).
|
|
21
|
+
- **`Studio::Cable.safe_broadcast { … }`** — best-effort broadcast guard that
|
|
22
|
+
catches `StandardError` **and** `ScriptError`, so a cable failure never breaks
|
|
23
|
+
the caller (the exact hole that caused the SEV-1).
|
|
24
|
+
- **`Studio::Broadcastable`** — model concern with safe Turbo-Streams wrappers
|
|
25
|
+
(`safe_broadcast_replace_to`, `_append_to`, `_prepend_to`, `_update_to`,
|
|
26
|
+
`_remove_to`) every app should broadcast through.
|
|
27
|
+
|
|
7
28
|
## 0.9.0 — 2026-06-23
|
|
8
29
|
|
|
9
30
|
### Added
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Mixin for ActiveRecord models that broadcast Turbo Streams. It wraps
|
|
5
|
+
# turbo-rails' broadcast_*_to methods in Studio::Cable.safe_broadcast, so a cable
|
|
6
|
+
# failure (a Redis hiccup, a missing/misconfigured adapter) can NEVER break the
|
|
7
|
+
# model save / after_commit that triggered the broadcast — the SEV-1 guard, in
|
|
8
|
+
# one place. The host model already has the raw broadcast_*_to methods (turbo-rails
|
|
9
|
+
# includes Turbo::Broadcastable into ActiveRecord::Base); these are the SAFE
|
|
10
|
+
# variants every app should broadcast through.
|
|
11
|
+
#
|
|
12
|
+
# class Task < ApplicationRecord
|
|
13
|
+
# include Studio::Broadcastable
|
|
14
|
+
# after_create_commit { safe_broadcast_replace_to [:board], target: "card_#{id}",
|
|
15
|
+
# partial: "tasks/card", locals: { task: self } }
|
|
16
|
+
# end
|
|
17
|
+
module Broadcastable
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
def safe_broadcast_replace_to(*args, **kwargs, &block)
|
|
21
|
+
Studio::Cable.safe_broadcast { broadcast_replace_to(*args, **kwargs, &block) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def safe_broadcast_update_to(*args, **kwargs, &block)
|
|
25
|
+
Studio::Cable.safe_broadcast { broadcast_update_to(*args, **kwargs, &block) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def safe_broadcast_append_to(*args, **kwargs, &block)
|
|
29
|
+
Studio::Cable.safe_broadcast { broadcast_append_to(*args, **kwargs, &block) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def safe_broadcast_prepend_to(*args, **kwargs, &block)
|
|
33
|
+
Studio::Cable.safe_broadcast { broadcast_prepend_to(*args, **kwargs, &block) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def safe_broadcast_remove_to(*args, **kwargs, &block)
|
|
37
|
+
Studio::Cable.safe_broadcast { broadcast_remove_to(*args, **kwargs, &block) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/studio/cable.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "redis"
|
|
4
|
+
|
|
5
|
+
module Studio
|
|
6
|
+
# Shared ActionCable / Turbo-Streams helpers for host apps.
|
|
7
|
+
module Cable
|
|
8
|
+
# Run a broadcast best-effort: a cable failure must NEVER break the caller (the
|
|
9
|
+
# model save / task write that triggered it). ScriptError is caught ON PURPOSE —
|
|
10
|
+
# a missing or misconfigured cable adapter raises Gem::LoadError, which is a
|
|
11
|
+
# ScriptError, NOT a StandardError. A plain `rescue StandardError` let exactly
|
|
12
|
+
# that escape an after_commit and 500 every task write in production (the SEV-1
|
|
13
|
+
# this primitive exists to prevent). Failures are captured to ErrorLog when the
|
|
14
|
+
# host defines it, else logged. Returns nil on failure.
|
|
15
|
+
#
|
|
16
|
+
# The never-raise guarantee is ABSOLUTE: even logging the failure must not break
|
|
17
|
+
# the caller. ErrorLog.capture! writes to the DB and can itself raise (e.g.
|
|
18
|
+
# ActiveRecord::NoDatabaseError when the DB is down), so the logging is wrapped
|
|
19
|
+
# in its own rescue — the guard cannot be defeated by its own error path.
|
|
20
|
+
def self.safe_broadcast
|
|
21
|
+
yield
|
|
22
|
+
rescue StandardError, ScriptError => e
|
|
23
|
+
begin
|
|
24
|
+
if defined?(ErrorLog) && ErrorLog.respond_to?(:capture!)
|
|
25
|
+
ErrorLog.capture!(e)
|
|
26
|
+
elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
27
|
+
Rails.logger.warn("[studio-cable] broadcast failed (non-fatal): #{e.class}: #{e.message}")
|
|
28
|
+
end
|
|
29
|
+
rescue StandardError, ScriptError
|
|
30
|
+
nil # logging is best-effort too — the never-raise guarantee is absolute
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/studio/redis.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Studio
|
|
6
|
+
# Single source of Redis connection truth for EVERY Redis client in a host app —
|
|
7
|
+
# ActionCable (config/cable.yml), the Rails cache_store, and Sidekiq. It bakes in
|
|
8
|
+
# the Heroku gotcha so no app re-hits it:
|
|
9
|
+
#
|
|
10
|
+
# Heroku Redis serves rediss:// (TLS) with a SELF-SIGNED cert. redis-client
|
|
11
|
+
# verifies peer certs by default, so the connection is silently REJECTED — and
|
|
12
|
+
# because ActionCable's pubsub failure is silent (the /cable socket still
|
|
13
|
+
# upgrades to 101), broadcasts simply never reach subscribers; the cache no-ops.
|
|
14
|
+
# The fix is ssl_params verify_mode VERIFY_NONE, applied automatically here for
|
|
15
|
+
# any rediss:// URL.
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# cable.yml -> Studio::Redis.options (url + ssl_params)
|
|
19
|
+
# cache_store -> Studio::Redis.options(namespace: "x", expires_in: 90.minutes)
|
|
20
|
+
# sidekiq -> Studio::Redis.options
|
|
21
|
+
module Redis
|
|
22
|
+
# The Redis URL from the environment, with a local-dev fallback.
|
|
23
|
+
def self.url(default = "redis://localhost:6379/1")
|
|
24
|
+
ENV.fetch("REDIS_URL", default)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# True when the endpoint is TLS (Heroku Redis), i.e. a rediss:// URL.
|
|
28
|
+
def self.tls?(redis_url = url)
|
|
29
|
+
redis_url.to_s.start_with?("rediss://")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Connection options for a Redis client: the url, plus — for a TLS endpoint —
|
|
33
|
+
# the self-signed-cert handling Heroku requires. Caller extras (namespace,
|
|
34
|
+
# reconnect_attempts, error_handler, …) merge through untouched.
|
|
35
|
+
def self.options(redis_url: url, **extra)
|
|
36
|
+
opts = { url: redis_url }.merge(extra)
|
|
37
|
+
opts[:ssl_params] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } if tls?(redis_url)
|
|
38
|
+
opts
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
data/studio-engine.gemspec
CHANGED
|
@@ -28,4 +28,9 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.add_dependency "aws-sdk-s3", "~> 1.218"
|
|
29
29
|
spec.add_dependency "mini_magick", "~> 5.0"
|
|
30
30
|
spec.add_dependency "resend", "~> 1.1"
|
|
31
|
+
# Realtime: the redis cable/cache/Sidekiq adapter (Studio::Redis) + Turbo Streams
|
|
32
|
+
# broadcasting (Studio::Broadcastable). `redis` is the dependency whose ABSENCE
|
|
33
|
+
# 500'd a host app's task board — declaring it here makes that impossible to repeat.
|
|
34
|
+
spec.add_dependency "redis", ">= 4.0.1"
|
|
35
|
+
spec.add_dependency "turbo-rails", ">= 1.0"
|
|
31
36
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -126,6 +125,34 @@ dependencies:
|
|
|
126
125
|
- - "~>"
|
|
127
126
|
- !ruby/object:Gem::Version
|
|
128
127
|
version: '1.1'
|
|
128
|
+
- !ruby/object:Gem::Dependency
|
|
129
|
+
name: redis
|
|
130
|
+
requirement: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: 4.0.1
|
|
135
|
+
type: :runtime
|
|
136
|
+
prerelease: false
|
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
138
|
+
requirements:
|
|
139
|
+
- - ">="
|
|
140
|
+
- !ruby/object:Gem::Version
|
|
141
|
+
version: 4.0.1
|
|
142
|
+
- !ruby/object:Gem::Dependency
|
|
143
|
+
name: turbo-rails
|
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - ">="
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: '1.0'
|
|
149
|
+
type: :runtime
|
|
150
|
+
prerelease: false
|
|
151
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: '1.0'
|
|
129
156
|
description: Studio Engine is a non-isolated Rails engine that ships an opinionated
|
|
130
157
|
authentication + SSO contract, a polymorphic ErrorLog model, a Sluggable concern,
|
|
131
158
|
a 7-role dynamic theme system with CSS-custom-property generation, and an S3-backed
|
|
@@ -169,6 +196,7 @@ files:
|
|
|
169
196
|
- app/mailers/application_mailer.rb
|
|
170
197
|
- app/mailers/user_mailer.rb
|
|
171
198
|
- app/models/concerns/sluggable.rb
|
|
199
|
+
- app/models/concerns/studio/broadcastable.rb
|
|
172
200
|
- app/models/current.rb
|
|
173
201
|
- app/models/error_log.rb
|
|
174
202
|
- app/models/image_cache.rb
|
|
@@ -239,6 +267,7 @@ files:
|
|
|
239
267
|
- db/migrate/20260623130000_create_studio_enumerals.rb
|
|
240
268
|
- lib/studio-engine.rb
|
|
241
269
|
- lib/studio.rb
|
|
270
|
+
- lib/studio/cable.rb
|
|
242
271
|
- lib/studio/color_scale.rb
|
|
243
272
|
- lib/studio/email.rb
|
|
244
273
|
- lib/studio/email_smoke.rb
|
|
@@ -246,6 +275,7 @@ files:
|
|
|
246
275
|
- lib/studio/image_cache.rb
|
|
247
276
|
- lib/studio/link_token.rb
|
|
248
277
|
- lib/studio/mail_transport.rb
|
|
278
|
+
- lib/studio/redis.rb
|
|
249
279
|
- lib/studio/s3.rb
|
|
250
280
|
- lib/studio/theme_resolver.rb
|
|
251
281
|
- lib/studio/ui_primitives.rb
|
|
@@ -263,7 +293,6 @@ metadata:
|
|
|
263
293
|
source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
|
|
264
294
|
bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
|
|
265
295
|
changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
|
|
266
|
-
post_install_message:
|
|
267
296
|
rdoc_options: []
|
|
268
297
|
require_paths:
|
|
269
298
|
- lib
|
|
@@ -278,8 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
278
307
|
- !ruby/object:Gem::Version
|
|
279
308
|
version: '0'
|
|
280
309
|
requirements: []
|
|
281
|
-
rubygems_version:
|
|
282
|
-
signing_key:
|
|
310
|
+
rubygems_version: 4.0.9
|
|
283
311
|
specification_version: 4
|
|
284
312
|
summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
|
|
285
313
|
image caching
|