jumbotron 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 727d9ee2cecff54934b335484762a677ebfa7ee2bfe4a7f7dc408e04b0f7f6b0
4
- data.tar.gz: 3c16c45a725c58a7fc654172819752a9c4e69fd06f554353f51c8e127feb7eca
3
+ metadata.gz: 71a38ed750f1392e528a1039b2dd5cb33383581951da87f8261602d8fc20618c
4
+ data.tar.gz: b4d80cc8bbeeb0260f133838dd53d48f92315515be6f7e860e8c0ffb3d8526cd
5
5
  SHA512:
6
- metadata.gz: c7334f554705bccc4937c73975a77defce7bf08f0aeb23d984652eaa05cc4e43ddcecb0d1f3414a25f0e970f0dc34573e0adbbfdbc489252aeee6156b2e69a78
7
- data.tar.gz: a02bd5b23d138019d874124ac178652a8a8a936e238a1d1d9f2d54939903d03ea26accda1e72c90ba6899652f0fe41c73fd9593377b8b01ed0a8ef9889fb3654
6
+ metadata.gz: 9b5cdcb6ff223603581417cb047916d47675843b2758305240539dd596be15b20be07bf2c8056d4c46bb683b69bc3554f7b28f29e98f3cf4affbab401eddc6b4
7
+ data.tar.gz: '09e5f5b8f0edd4eae5c12a6443841193ad669d87736ac360cc63c37e1c5f02b0aa40eeeda25a224f7c1b45884602f721464bfb7b312b65667700a00869c61939'
@@ -51,7 +51,7 @@ module Jumbotron
51
51
 
52
52
  policy :live,
53
53
  type: :live_game_update,
54
- cadence: Cadence.new(every: 1, unit: :minute),
54
+ cadence: Cadence.new(every: 30, unit: :second),
55
55
  eligible: lambda { |game, now:|
56
56
  return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
57
57
  return false if INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
@@ -48,7 +48,7 @@ module Jumbotron
48
48
 
49
49
  policy :live,
50
50
  type: :live_game_update,
51
- cadence: Cadence.new(every: 1, unit: :minute),
51
+ cadence: Cadence.new(every: 30, unit: :second),
52
52
  eligible: lambda { |game, now:|
53
53
  return false if TERMINAL_LIFECYCLES.include?(game.lifecycle)
54
54
  return false if INTERRUPTED_LIFECYCLES.include?(game.lifecycle)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class Games < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ raw = unwrap(fetch_param(params, :game_ids))
9
+ return raw if deserializer_result?(raw)
10
+ return failure(errors: [{ code: "invalid_request", message: "game_ids is required" }]) if raw.nil?
11
+ unless raw.is_a?(Array)
12
+ return failure(errors: [{ code: "invalid_request", message: "game_ids must be an array" }])
13
+ end
14
+
15
+ if raw.size > Jumbotron::Public::MAX_GAMES_IDS
16
+ return failure(
17
+ errors: [{
18
+ code: "invalid_request",
19
+ message: "game_ids exceeds maximum of #{Jumbotron::Public::MAX_GAMES_IDS}"
20
+ }]
21
+ )
22
+ end
23
+
24
+ ids = []
25
+ raw.each do |item|
26
+ coerced = unwrap(require_integer(item, field: "game_ids", min: 1))
27
+ return coerced if deserializer_result?(coerced)
28
+
29
+ ids << coerced
30
+ end
31
+
32
+ include_raw = unwrap(fetch_param(params, :include))
33
+ return include_raw if deserializer_result?(include_raw)
34
+
35
+ includes = unwrap(Includes.parse(include_raw))
36
+ return includes if deserializer_result?(includes)
37
+
38
+ success(Jumbotron::Public::GamesRequest.new(game_ids: ids.uniq, includes: includes))
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Deserializers
5
+ module Public
6
+ class SandboxProjection < CommandTower::Deserializers::ApplicationDeserializer
7
+ def call(params)
8
+ name = unwrap(fetch_param(params, :name))
9
+ return name if deserializer_result?(name)
10
+ return failure(errors: [{ code: "invalid_request", message: "name is required" }]) if name.blank?
11
+
12
+ success(Jumbotron::Public::SandboxProjectionRequest.new(name: name.to_s))
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -13,7 +13,8 @@ module Jumbotron
13
13
  SandboxScheduleSelector = Data.define(:sport, :league, :season, :season_phase)
14
14
  RegisterSandboxProjectionRequest = Data.define(:name, :source)
15
15
  ResetSandboxProjectionRequest = Data.define(:sandbox, :start_week, :anchor_day, :now)
16
- SandboxProjection = Data.define(:name, :source, :sandbox)
16
+ SandboxProjectionRequest = Data.define(:name)
17
+ SandboxProjection = Data.define(:name, :source_label, :source, :sandbox)
17
18
  SandboxProjectionReset = Data.define(
18
19
  :projection,
19
20
  :source_anchor_date,
@@ -100,6 +101,10 @@ module Jumbotron
100
101
  )
101
102
  GameRequest = Data.define(:id, :includes)
102
103
  GameIdsRequest = Data.define(:game_ids)
104
+ # Bulk Game read — game_ids + optional includes (parity with #game / #schedule).
105
+ # Distinct from GameIdsRequest so consensus/current_lines stay include-free.
106
+ GamesRequest = Data.define(:game_ids, :includes)
107
+ MAX_GAMES_IDS = 64
103
108
  ScheduleRequest = Data.define(
104
109
  :league_id,
105
110
  :sport,
@@ -10,8 +10,9 @@ module Jumbotron
10
10
 
11
11
  def materialize(schedules, **)
12
12
  klass = job_class
13
- remove_stale(klass, schedules)
14
- upsert_desired(klass, schedules)
13
+ applicable = sidekiq_expressible(schedules)
14
+ remove_stale(klass, applicable)
15
+ upsert_desired(klass, applicable)
15
16
  end
16
17
 
17
18
  def cron_string(cadence)
@@ -20,6 +21,15 @@ module Jumbotron
20
21
 
21
22
  private
22
23
 
24
+ # Five-field cron cannot express sub-minute cadences (Solid Queue / fugit can).
25
+ def sidekiq_expressible(schedules)
26
+ schedules.reject { |definition| second_unit?(definition.cadence.unit) }
27
+ end
28
+
29
+ def second_unit?(unit)
30
+ Clock.canonical_unit(unit) == :second
31
+ end
32
+
23
33
  def remove_stale(klass, schedules)
24
34
  desired_ids = schedules.map(&:id)
25
35
  Array(klass.all).each do |job|
@@ -9,6 +9,7 @@ module Jumbotron
9
9
  def call
10
10
  context.public_projection = Jumbotron::Public::SandboxProjection.new(
11
11
  name: projection.name,
12
+ source_label: source_label,
12
13
  source: selector(projection.source_season, projection.source_anchor_phase.name),
13
14
  sandbox: selector(projection.sandbox_season, projection.source_anchor_phase.name)
14
15
  )
@@ -16,6 +17,14 @@ module Jumbotron
16
17
 
17
18
  private
18
19
 
20
+ def source_label
21
+ [
22
+ projection.source_season.league.name.upcase,
23
+ projection.source_season.name,
24
+ projection.source_anchor_phase.name.titleize
25
+ ].join(" ")
26
+ end
27
+
19
28
  def selector(season, phase_name)
20
29
  Jumbotron::Public::SandboxScheduleSelector.new(
21
30
  sport: season.league.sport.name,
@@ -30,11 +30,20 @@ module Jumbotron
30
30
  def projection_identity
31
31
  Jumbotron::Public::SandboxProjection.new(
32
32
  name: projection.name,
33
+ source_label: source_label,
33
34
  source: selector(projection.source_season, projection.source_anchor_phase.name),
34
35
  sandbox: selector(projection.sandbox_season, projection.source_anchor_phase.name)
35
36
  )
36
37
  end
37
38
 
39
+ def source_label
40
+ [
41
+ projection.source_season.league.name.upcase,
42
+ projection.source_season.name,
43
+ projection.source_anchor_phase.name.titleize
44
+ ].join(" ")
45
+ end
46
+
38
47
  def selector(season, phase_name)
39
48
  Jumbotron::Public::SandboxScheduleSelector.new(
40
49
  sport: season.league.sport.name,
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Services
5
+ module Public
6
+ class ReadSandboxProjection < CommandTower::Services::ApplicationService
7
+ validate :name, required: true
8
+
9
+ def call
10
+ projection = Jumbotron::SandboxProjection.includes(
11
+ :source_anchor_phase,
12
+ source_season: { league: :sport },
13
+ sandbox_season: { league: :sport }
14
+ ).find_by(name:)
15
+ return reject if projection.nil?
16
+
17
+ context.projection = projection
18
+ end
19
+
20
+ private
21
+
22
+ def reject
23
+ context.fail!(
24
+ application_error: Jumbotron::Errors::Public::NotFoundError.new(
25
+ details: { code: "sandbox_not_found", message: "sandbox projection was not found" }
26
+ )
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Workflows
5
+ class ReadGamesWorkflow < CommandTower::Workflows::ApplicationWorkflow
6
+ include PublicRead
7
+
8
+ retry_strategy :none
9
+
10
+ def call(**kwargs)
11
+ deserialized = Deserializers::Public::Games.call(kwargs)
12
+ return map_deserializer_failure(deserialized) unless deserialized.success?
13
+
14
+ request = deserialized.input
15
+ loaded = Services::Public::LoadGamesByIds.call(game_ids: request.game_ids)
16
+ return map_service_failure(loaded) unless loaded.success?
17
+
18
+ games = Array(loaded.data[:games])
19
+ included = Services::Public::LoadIncludedLines.call(games: games, includes: request.includes)
20
+ return map_service_failure(included) unless included.success?
21
+
22
+ public_games = games.map do |game|
23
+ assembled = Services::Public::AssemblePublicGame.call(
24
+ game: game,
25
+ includes: request.includes,
26
+ current_lines: included.data[:current_lines],
27
+ consensus_lines: included.data[:consensus_lines]
28
+ )
29
+ return map_service_failure(assembled) unless assembled.success?
30
+
31
+ assembled.data[:public_game]
32
+ end
33
+
34
+ success(payload: { games: public_games }, http_status: :ok)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jumbotron
4
+ module Workflows
5
+ module Sandbox
6
+ class ReadProjectionWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ include Jumbotron::Workflows::PublicRead
8
+
9
+ retry_strategy :none
10
+
11
+ def call(**kwargs)
12
+ deserialized = Jumbotron::Deserializers::Public::SandboxProjection.call(kwargs)
13
+ return map_deserializer_failure(deserialized) unless deserialized.success?
14
+
15
+ read = Jumbotron::Services::Public::ReadSandboxProjection.call(name: deserialized.input.name)
16
+ return map_service_failure(read) unless read.success?
17
+
18
+ assembled = Jumbotron::Services::Public::AssembleSandboxProjection.call(
19
+ projection: read.data[:projection]
20
+ )
21
+ return map_service_failure(assembled) unless assembled.success?
22
+
23
+ success(payload: { sandbox_projection: assembled.data[:public_projection] }, http_status: :ok)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # Upgrade: Jumbotron 0.3.0
2
+
3
+ **From:** `0.2.0`
4
+ **To:** `0.3.0`
5
+
6
+ Minor release: additive public read access for named sandbox projections and stable source labeling for host administration.
7
+
8
+ ## Host-facing change
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | `Jumbotron.client.sandbox_projection` | Reads a named sandbox projection through the public workflow/client boundary without exposing persistence. Unknown names return the existing structured public failure contract. |
13
+ | `SandboxProjection#source_label` | Public projection DTOs, including register/Reset results, now include a host-ready source label such as `NFL 2026 Regular Season`. |
14
+ | Public read validation | Named projection reads use the same deserializer/service/workflow architecture as existing public operations. |
15
+
16
+ ## Upgrade checklist
17
+
18
+ 1. Bump the gem to `0.3.0` (or `>= 0.3.0`).
19
+ 2. No new migration. Hosts already on `0.2.0` do not need to copy or run Jumbotron migrations for this release.
20
+ 3. Hosts may read the configured source before offering a destructive Reset:
21
+
22
+ ```ruby
23
+ projection = Jumbotron.client.sandbox_projection(name: "nfl-sandbox")
24
+ projection.source_label
25
+ ```
26
+
27
+ ## What is not in this release
28
+
29
+ - No sandbox projection or Reset persistence change.
30
+ - No ESPN public Game/Schedule/Consensus/CurrentLines contract change.
31
+ - No Pick'em administration UI or competitive-domain behavior.
@@ -0,0 +1,31 @@
1
+ # Upgrade: Jumbotron 0.4.0
2
+
3
+ **From:** `0.3.0`
4
+ **To:** `0.4.0`
5
+
6
+ Minor release: adds a bulk public Game read, tightens live-game polling cadence to 30 seconds, and keeps the Sidekiq Cron scheduling backend safe for sub-minute cadences.
7
+
8
+ ## Host-facing change
9
+
10
+ | Change | Host impact |
11
+ |--------|-------------|
12
+ | `Jumbotron.client.games(game_ids:, include:)` | Bulk public Game read (parity with `#game` / `#schedule`). Accepts up to 64 `game_ids`, dedupes and preserves request order, silently omits unknown ids, and composes `current_lines` / `consensus_lines` through the same `include` contract as other public reads. Requesting more than 64 ids raises `Jumbotron::InvalidRequestError`. |
13
+ | ESPN and Sandbox `:live` adapter cadence | Live-game polling moved from every 1 minute to every 30 seconds. Hosts running the ESPN or Sandbox NFL adapters will see more frequent live-update scheduling. |
14
+ | Sidekiq Cron scheduling backend | Sub-minute (`:second`) cadences are now excluded from the five-field cron materialization (five-field cron cannot express sub-minute schedules). Hosts on the Sidekiq Cron backend for a 30-second-cadence adapter must run the Solid Queue backend (or another cadence-aware backend) for that schedule to actually fire; Sidekiq Cron silently skips it rather than erroring. |
15
+
16
+ ## Upgrade checklist
17
+
18
+ 1. Bump the gem to `0.4.0` (or `>= 0.4.0`).
19
+ 2. No new migration. Hosts already on `0.3.0` do not need to copy or run Jumbotron migrations for this release.
20
+ 3. Hosts using the Sidekiq Cron scheduling backend with the ESPN or Sandbox NFL adapter should confirm their live-game schedule is served by a backend that supports sub-minute cadence (e.g. Solid Queue), since Sidekiq Cron now omits it rather than materializing an incorrect once-per-minute job.
21
+ 4. Hosts may call the new bulk read directly:
22
+
23
+ ```ruby
24
+ games = Jumbotron.client.games(game_ids: [101, 102, 103], include: [:consensus])
25
+ ```
26
+
27
+ ## What is not in this release
28
+
29
+ - No sandbox projection or Reset persistence change.
30
+ - No ESPN public Schedule/CurrentLines contract change beyond the `:live` cadence value.
31
+ - No Pick'em administration UI or competitive-domain behavior.
@@ -2,6 +2,13 @@
2
2
 
3
3
  module Jumbotron
4
4
  class Client
5
+ def sandbox_projection(**)
6
+ Public::TranslateOutcome.call(
7
+ Workflows::Sandbox::ReadProjectionWorkflow.call(**),
8
+ payload_key: :sandbox_projection
9
+ )
10
+ end
11
+
5
12
  def register_sandbox_projection(**)
6
13
  Public::TranslateOutcome.call(
7
14
  Workflows::Sandbox::RegisterProjectionWorkflow.call(**),
@@ -37,6 +44,13 @@ module Jumbotron
37
44
  )
38
45
  end
39
46
 
47
+ def games(**)
48
+ Public::TranslateOutcome.call(
49
+ Workflows::ReadGamesWorkflow.call(**),
50
+ payload_key: :games
51
+ )
52
+ end
53
+
40
54
  def consensus(**)
41
55
  Public::TranslateOutcome.call(
42
56
  Workflows::ReadConsensusWorkflow.call(**),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jumbotron
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jumbotron
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - matt-taylor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-09 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: command_tower
@@ -130,9 +130,11 @@ files:
130
130
  - app/deserializers/jumbotron/deserializers/clients/espn/week.rb
131
131
  - app/deserializers/jumbotron/deserializers/public/game.rb
132
132
  - app/deserializers/jumbotron/deserializers/public/game_ids.rb
133
+ - app/deserializers/jumbotron/deserializers/public/games.rb
133
134
  - app/deserializers/jumbotron/deserializers/public/includes.rb
134
135
  - app/deserializers/jumbotron/deserializers/public/register_sandbox_projection.rb
135
136
  - app/deserializers/jumbotron/deserializers/public/reset_sandbox_projection.rb
137
+ - app/deserializers/jumbotron/deserializers/public/sandbox_projection.rb
136
138
  - app/deserializers/jumbotron/deserializers/public/schedule.rb
137
139
  - app/deserializers/jumbotron/deserializers/public/schedule_groups.rb
138
140
  - app/errors/jumbotron/error.rb
@@ -227,6 +229,7 @@ files:
227
229
  - app/services/jumbotron/services/public/map_public_consensus_lines.rb
228
230
  - app/services/jumbotron/services/public/map_public_current_lines.rb
229
231
  - app/services/jumbotron/services/public/read_game.rb
232
+ - app/services/jumbotron/services/public/read_sandbox_projection.rb
230
233
  - app/services/jumbotron/services/public/read_schedule.rb
231
234
  - app/services/jumbotron/services/public/read_schedule_groups.rb
232
235
  - app/services/jumbotron/services/public/resolve_current_lines_for_games.rb
@@ -247,8 +250,10 @@ files:
247
250
  - app/workflows/jumbotron/workflows/read_consensus_workflow.rb
248
251
  - app/workflows/jumbotron/workflows/read_current_lines_workflow.rb
249
252
  - app/workflows/jumbotron/workflows/read_game_workflow.rb
253
+ - app/workflows/jumbotron/workflows/read_games_workflow.rb
250
254
  - app/workflows/jumbotron/workflows/read_schedule_groups_workflow.rb
251
255
  - app/workflows/jumbotron/workflows/read_schedule_workflow.rb
256
+ - app/workflows/jumbotron/workflows/sandbox/read_projection_workflow.rb
252
257
  - app/workflows/jumbotron/workflows/sandbox/register_projection_workflow.rb
253
258
  - app/workflows/jumbotron/workflows/sandbox/reset_projection_workflow.rb
254
259
  - app/workflows/jumbotron/workflows/synchronize_adapter_workflow.rb
@@ -269,6 +274,8 @@ files:
269
274
  - docs/upgrades/0.1.3.md
270
275
  - docs/upgrades/0.1.4.md
271
276
  - docs/upgrades/0.2.0.md
277
+ - docs/upgrades/0.3.0.md
278
+ - docs/upgrades/0.4.0.md
272
279
  - lib/jumbotron.rb
273
280
  - lib/jumbotron/client.rb
274
281
  - lib/jumbotron/engine.rb