Skip to content

Commit d2aa933

Browse files
authored
Merge pull request Sofie-Automation#1291 from bbc/upstream/fix-switchrouteset-updating-timeline
2 parents 1795ea9 + 082b1d8 commit d2aa933

File tree

19 files changed

+853
-314
lines changed

19 files changed

+853
-314
lines changed

packages/corelib/src/overrideOpHelper.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,15 @@ export interface OverrideOpHelperBatcher extends OverrideOpHelperForItemContents
162162
export type OverrideOpHelper = () => OverrideOpHelperBatcher
163163

164164
export class OverrideOpHelperImpl implements OverrideOpHelperBatcher {
165-
readonly #saveOverrides: SaveOverridesFunction
165+
readonly #saveOverrides: SaveOverridesFunction | null
166166
readonly #object: ObjectWithOverrides<any>
167167

168-
constructor(saveOverrides: SaveOverridesFunction, object: ObjectWithOverrides<any>) {
168+
constructor(
169+
saveOverrides: SaveOverridesFunction | null,
170+
object: ObjectWithOverrides<any> | ReadonlyDeep<ObjectWithOverrides<any>>
171+
) {
169172
this.#saveOverrides = saveOverrides
170-
this.#object = { ...object }
173+
this.#object = { defaults: object.defaults, overrides: [...object.overrides] }
171174
}
172175

173176
clearItemOverrides = (itemId: string, subPath: string): this => {
@@ -314,6 +317,12 @@ export class OverrideOpHelperImpl implements OverrideOpHelperBatcher {
314317
}
315318

316319
commit = (): void => {
320+
if (!this.#saveOverrides) throw new Error('Cannot commit changes without a save function')
321+
317322
this.#saveOverrides(this.#object.overrides)
318323
}
324+
325+
getPendingOps = (): SomeObjectOverrideOp[] => {
326+
return this.#object.overrides
327+
}
319328
}

packages/job-worker/src/__mocks__/context.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ export class MockJobContext implements JobContext {
226226
// throw new Error('Method not implemented.')
227227
}
228228

229+
setRouteSetActive(_routeSetId: string, _isActive: boolean | 'toggle'): boolean {
230+
throw new Error('Method not implemented.')
231+
}
232+
233+
async saveRouteSetChanges(): Promise<void> {
234+
// throw new Error('Method not implemented.')
235+
}
236+
237+
discardRouteSetChanges(): void {
238+
// throw new Error('Method not implemented.')
239+
}
240+
229241
/**
230242
* Mock methods
231243
*/

packages/job-worker/src/jobs/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,26 @@ export interface JobContext extends StudioCacheContext {
6464

6565
/** Hack: fast-track the timeline out to the playout-gateway. */
6666
hackPublishTimelineToFastTrack(newTimeline: TimelineComplete): void
67+
68+
/**
69+
* Set whether a routeset for this studio is active.
70+
* Any routeset `exclusivityGroup` will be respected.
71+
* The changes will be immediately visible in subsequent calls to the `studio` getter
72+
* @param routeSetId The routeSetId to change
73+
* @param isActive Whether the routeSet should be active, or toggle
74+
* @returns Whether the change could affect playout
75+
*/
76+
setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean
77+
78+
/**
79+
* Save any changes to the routesets for this studio to the database
80+
*/
81+
saveRouteSetChanges(): Promise<void>
82+
83+
/**
84+
* Discard any unsaved changes to the routesets for this studio
85+
*/
86+
discardRouteSetChanges(): void
6787
}
6888

6989
/**

packages/job-worker/src/playout/abPlayback/__tests__/routeSetDisabling.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@ describe('route set disabling ab players', () => {
2626
expect(result).toEqual(DEFAULT_PLAYERS)
2727
})
2828

29+
test('mismatch of playerId types', () => {
30+
const routesets: Record<string, StudioRouteSet> = {
31+
route1: {
32+
name: '',
33+
active: false,
34+
behavior: StudioRouteBehavior.TOGGLE,
35+
routes: [],
36+
abPlayers: [
37+
{
38+
poolName: POOL_NAME,
39+
playerId: '1', // because ui field is always a string
40+
},
41+
],
42+
},
43+
}
44+
45+
const players: ABPlayerDefinition[] = [
46+
{
47+
playerId: 1, // number because blueprint defined it as such
48+
},
49+
{ playerId: 2 },
50+
]
51+
52+
const result = runDisablePlayersFiltering(routesets, players)
53+
54+
const expectedPlayers = players.filter((p) => p.playerId !== 1)
55+
expect(result).toEqual(expectedPlayers)
56+
})
57+
2958
describe('single routeset per player', () => {
3059
const ROUTESETS_SEPARATE: Record<string, StudioRouteSet> = {
3160
pl1: {

packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { ABPlayerDefinition, AbPlayerId } from '@sofie-automation/blueprints-integration'
1+
import type { ABPlayerDefinition } from '@sofie-automation/blueprints-integration'
22
import type { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio'
33
import { logger } from '../../logging'
44

55
/**
66
* Map<poolName, Map<playerId, disablePlayer>>
7+
* Note: this explicitly uses a string for the playerId, to avoid issues with types for values from the ui
78
*/
8-
type MembersOfRouteSets = Map<string, Map<AbPlayerId, boolean>>
9+
type MembersOfRouteSets = Map<string, Map<string, boolean>>
910

1011
export function findPlayersInRouteSets(routeSets: Record<string, StudioRouteSet>): MembersOfRouteSets {
1112
const routeSetEnabledPlayers: MembersOfRouteSets = new Map()
@@ -18,8 +19,8 @@ export function findPlayersInRouteSets(routeSets: Record<string, StudioRouteSet>
1819
}
1920

2021
// Make sure player is marked as enabled
21-
const currentState = poolEntry.get(abPlayer.playerId)
22-
poolEntry.set(abPlayer.playerId, currentState || routeSet.active)
22+
const currentState = poolEntry.get(String(abPlayer.playerId))
23+
poolEntry.set(String(abPlayer.playerId), currentState || routeSet.active)
2324
}
2425
}
2526
return routeSetEnabledPlayers
@@ -35,7 +36,7 @@ export function abPoolFilterDisabled(
3536

3637
// Filter out any disabled players:
3738
return players.filter((player) => {
38-
const playerState = poolRouteSetEnabledPlayers.get(player.playerId)
39+
const playerState = poolRouteSetEnabledPlayers.get(String(player.playerId))
3940
if (playerState === false) {
4041
logger.silly(`AB Pool ${poolName} playerId : ${player.playerId} are disabled`)
4142
return false

packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
481481
}
482482

483483
switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean {
484-
return this.#baselineHelper.updateRouteSetActive(routeSetId, isActive)
484+
return this.context.setRouteSetActive(routeSetId, isActive)
485485
}
486486

487487
cycleSelectedPartInstances(): void {
@@ -645,6 +645,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
645645
...writePartInstancesAndPieceInstances(this.context, this.allPartInstances),
646646
writeAdlibTestingSegments(this.context, this.rundownsImpl),
647647
this.#baselineHelper.saveAllToDatabase(),
648+
this.context.saveRouteSetChanges(),
648649
])
649650

650651
this.#playlistHasChanged = false

packages/job-worker/src/studio/model/StudioBaselineHelper.ts

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,19 @@ import {
66
} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages'
77
import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem'
88
import { saveIntoDb } from '../../db/changes'
9-
import { StudioRouteBehavior, StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio'
10-
import { logger } from '../../logging'
11-
import {
12-
WrappedOverridableItemNormal,
13-
getAllCurrentItemsFromOverrides,
14-
OverrideOpHelperImpl,
15-
} from '@sofie-automation/corelib/dist/overrideOpHelper'
16-
import { ObjectWithOverrides, SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
179

1810
export class StudioBaselineHelper {
1911
readonly #context: JobContext
2012

21-
#overridesRouteSetBuffer: ObjectWithOverrides<Record<string, StudioRouteSet>>
2213
#pendingExpectedPackages: ExpectedPackageDBFromStudioBaselineObjects[] | undefined
2314
#pendingExpectedPlayoutItems: ExpectedPlayoutItemStudio[] | undefined
24-
#routeSetChanged: boolean
2515

2616
constructor(context: JobContext) {
2717
this.#context = context
28-
this.#overridesRouteSetBuffer = { ...context.studio.routeSetsWithOverrides } as ObjectWithOverrides<
29-
Record<string, StudioRouteSet>
30-
>
31-
this.#routeSetChanged = false
3218
}
3319

3420
hasChanges(): boolean {
35-
return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems || this.#routeSetChanged
21+
return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems
3622
}
3723

3824
setExpectedPackages(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void {
@@ -63,74 +49,9 @@ export class StudioBaselineHelper {
6349
this.#pendingExpectedPackages
6450
)
6551
: undefined,
66-
this.#routeSetChanged
67-
? this.#context.directCollections.Studios.update(
68-
{
69-
_id: this.#context.studioId,
70-
},
71-
{
72-
$set: { 'routeSetsWithOverrides.overrides': this.#overridesRouteSetBuffer.overrides },
73-
}
74-
)
75-
: undefined,
7652
])
7753

7854
this.#pendingExpectedPlayoutItems = undefined
7955
this.#pendingExpectedPackages = undefined
80-
this.#routeSetChanged = false
81-
this.#overridesRouteSetBuffer = { ...this.#context.studio.routeSetsWithOverrides } as ObjectWithOverrides<
82-
Record<string, StudioRouteSet>
83-
>
84-
}
85-
86-
updateRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean {
87-
const studio = this.#context.studio
88-
89-
const routeSets: WrappedOverridableItemNormal<StudioRouteSet>[] = getAllCurrentItemsFromOverrides(
90-
this.#overridesRouteSetBuffer,
91-
null
92-
)
93-
94-
const routeSet = routeSets.find((routeSet) => routeSet.id === routeSetId)
95-
96-
if (routeSet === undefined) throw new Error(`RouteSet "${routeSetId}" not found!`)
97-
98-
if (isActive === 'toggle') isActive = !routeSet.computed.active
99-
100-
if (routeSet.computed?.behavior === StudioRouteBehavior.ACTIVATE_ONLY && isActive === false)
101-
throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`)
102-
103-
const saveOverrides = (newOps: SomeObjectOverrideOp[]) => {
104-
this.#overridesRouteSetBuffer.overrides = newOps
105-
this.#routeSetChanged = true
106-
}
107-
const overrideHelper = new OverrideOpHelperImpl(saveOverrides, this.#overridesRouteSetBuffer)
108-
109-
// Track whether changing this routeset could affect how the timeline is generated, so that it can be following this update
110-
let mayAffectTimeline = couldRoutesetAffectTimelineGeneration(routeSet)
111-
112-
logger.debug(`switchRouteSet "${studio._id}" "${routeSet.id}"=${isActive}`)
113-
overrideHelper.setItemValue(routeSet.id, `active`, isActive)
114-
115-
// Deactivate other routeSets in the same exclusivity group:
116-
if (routeSet.computed.exclusivityGroup && isActive === true) {
117-
for (const [, otherRouteSet] of Object.entries<WrappedOverridableItemNormal<StudioRouteSet>>(routeSets)) {
118-
if (otherRouteSet.id === routeSet.id) continue
119-
if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) {
120-
logger.debug(`switchRouteSet Other ID "${studio._id}" "${otherRouteSet.id}"=false`)
121-
overrideHelper.setItemValue(otherRouteSet.id, `active`, false)
122-
123-
mayAffectTimeline = mayAffectTimeline || couldRoutesetAffectTimelineGeneration(otherRouteSet)
124-
}
125-
}
126-
}
127-
128-
overrideHelper.commit()
129-
130-
return mayAffectTimeline
13156
}
13257
}
133-
134-
function couldRoutesetAffectTimelineGeneration(routeSet: WrappedOverridableItemNormal<StudioRouteSet>): boolean {
135-
return routeSet.computed.abPlayers.length > 0
136-
}

packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel {
102102
}
103103

104104
switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean {
105-
return this.#baselineHelper.updateRouteSetActive(routeSetId, isActive)
105+
return this.context.setRouteSetActive(routeSetId, isActive)
106106
}
107107

108108
/**
@@ -125,7 +125,11 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel {
125125
}
126126
this.#timelineHasChanged = false
127127

128-
await this.#baselineHelper.saveAllToDatabase()
128+
await Promise.all([
129+
this.#baselineHelper.saveAllToDatabase(),
130+
this.context.saveRouteSetChanges(),
131+
//
132+
])
129133

130134
if (span) span.end()
131135
}

packages/job-worker/src/workers/caches.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { clone, deepFreeze } from '@sofie-automation/corelib/dist/lib'
1616
import { logger } from '../logging'
1717
import deepmerge = require('deepmerge')
1818
import { ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs'
19-
import { StudioCacheContextImpl } from './context'
19+
import { StudioCacheContextImpl } from './context/StudioCacheContextImpl'
2020

2121
/**
2222
* A Wrapper to maintain a cache and provide a context using the cache when appropriate

0 commit comments

Comments
 (0)