Skip to content

Commit

Permalink
Merge pull request nrkno#1291 from bbc/upstream/fix-switchrouteset-up…
Browse files Browse the repository at this point in the history
…dating-timeline
  • Loading branch information
jstarpl authored Oct 29, 2024
2 parents 1795ea9 + 082b1d8 commit d2aa933
Show file tree
Hide file tree
Showing 19 changed files with 853 additions and 314 deletions.
15 changes: 12 additions & 3 deletions packages/corelib/src/overrideOpHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,15 @@ export interface OverrideOpHelperBatcher extends OverrideOpHelperForItemContents
export type OverrideOpHelper = () => OverrideOpHelperBatcher

export class OverrideOpHelperImpl implements OverrideOpHelperBatcher {
readonly #saveOverrides: SaveOverridesFunction
readonly #saveOverrides: SaveOverridesFunction | null
readonly #object: ObjectWithOverrides<any>

constructor(saveOverrides: SaveOverridesFunction, object: ObjectWithOverrides<any>) {
constructor(
saveOverrides: SaveOverridesFunction | null,
object: ObjectWithOverrides<any> | ReadonlyDeep<ObjectWithOverrides<any>>
) {
this.#saveOverrides = saveOverrides
this.#object = { ...object }
this.#object = { defaults: object.defaults, overrides: [...object.overrides] }
}

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

commit = (): void => {
if (!this.#saveOverrides) throw new Error('Cannot commit changes without a save function')

this.#saveOverrides(this.#object.overrides)
}

getPendingOps = (): SomeObjectOverrideOp[] => {
return this.#object.overrides
}
}
12 changes: 12 additions & 0 deletions packages/job-worker/src/__mocks__/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@ export class MockJobContext implements JobContext {
// throw new Error('Method not implemented.')
}

setRouteSetActive(_routeSetId: string, _isActive: boolean | 'toggle'): boolean {
throw new Error('Method not implemented.')
}

async saveRouteSetChanges(): Promise<void> {
// throw new Error('Method not implemented.')
}

discardRouteSetChanges(): void {
// throw new Error('Method not implemented.')
}

/**
* Mock methods
*/
Expand Down
20 changes: 20 additions & 0 deletions packages/job-worker/src/jobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ export interface JobContext extends StudioCacheContext {

/** Hack: fast-track the timeline out to the playout-gateway. */
hackPublishTimelineToFastTrack(newTimeline: TimelineComplete): void

/**
* Set whether a routeset for this studio is active.
* Any routeset `exclusivityGroup` will be respected.
* The changes will be immediately visible in subsequent calls to the `studio` getter
* @param routeSetId The routeSetId to change
* @param isActive Whether the routeSet should be active, or toggle
* @returns Whether the change could affect playout
*/
setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean

/**
* Save any changes to the routesets for this studio to the database
*/
saveRouteSetChanges(): Promise<void>

/**
* Discard any unsaved changes to the routesets for this studio
*/
discardRouteSetChanges(): void
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ describe('route set disabling ab players', () => {
expect(result).toEqual(DEFAULT_PLAYERS)
})

test('mismatch of playerId types', () => {
const routesets: Record<string, StudioRouteSet> = {
route1: {
name: '',
active: false,
behavior: StudioRouteBehavior.TOGGLE,
routes: [],
abPlayers: [
{
poolName: POOL_NAME,
playerId: '1', // because ui field is always a string
},
],
},
}

const players: ABPlayerDefinition[] = [
{
playerId: 1, // number because blueprint defined it as such
},
{ playerId: 2 },
]

const result = runDisablePlayersFiltering(routesets, players)

const expectedPlayers = players.filter((p) => p.playerId !== 1)
expect(result).toEqual(expectedPlayers)
})

describe('single routeset per player', () => {
const ROUTESETS_SEPARATE: Record<string, StudioRouteSet> = {
pl1: {
Expand Down
11 changes: 6 additions & 5 deletions packages/job-worker/src/playout/abPlayback/routeSetDisabling.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { ABPlayerDefinition, AbPlayerId } from '@sofie-automation/blueprints-integration'
import type { ABPlayerDefinition } from '@sofie-automation/blueprints-integration'
import type { StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { logger } from '../../logging'

/**
* Map<poolName, Map<playerId, disablePlayer>>
* Note: this explicitly uses a string for the playerId, to avoid issues with types for values from the ui
*/
type MembersOfRouteSets = Map<string, Map<AbPlayerId, boolean>>
type MembersOfRouteSets = Map<string, Map<string, boolean>>

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

// Make sure player is marked as enabled
const currentState = poolEntry.get(abPlayer.playerId)
poolEntry.set(abPlayer.playerId, currentState || routeSet.active)
const currentState = poolEntry.get(String(abPlayer.playerId))
poolEntry.set(String(abPlayer.playerId), currentState || routeSet.active)
}
}
return routeSetEnabledPlayers
Expand All @@ -35,7 +36,7 @@ export function abPoolFilterDisabled(

// Filter out any disabled players:
return players.filter((player) => {
const playerState = poolRouteSetEnabledPlayers.get(player.playerId)
const playerState = poolRouteSetEnabledPlayers.get(String(player.playerId))
if (playerState === false) {
logger.silly(`AB Pool ${poolName} playerId : ${player.playerId} are disabled`)
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
}

switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean {
return this.#baselineHelper.updateRouteSetActive(routeSetId, isActive)
return this.context.setRouteSetActive(routeSetId, isActive)
}

cycleSelectedPartInstances(): void {
Expand Down Expand Up @@ -645,6 +645,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
...writePartInstancesAndPieceInstances(this.context, this.allPartInstances),
writeAdlibTestingSegments(this.context, this.rundownsImpl),
this.#baselineHelper.saveAllToDatabase(),
this.context.saveRouteSetChanges(),
])

this.#playlistHasChanged = false
Expand Down
81 changes: 1 addition & 80 deletions packages/job-worker/src/studio/model/StudioBaselineHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,19 @@ import {
} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages'
import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem'
import { saveIntoDb } from '../../db/changes'
import { StudioRouteBehavior, StudioRouteSet } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { logger } from '../../logging'
import {
WrappedOverridableItemNormal,
getAllCurrentItemsFromOverrides,
OverrideOpHelperImpl,
} from '@sofie-automation/corelib/dist/overrideOpHelper'
import { ObjectWithOverrides, SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'

export class StudioBaselineHelper {
readonly #context: JobContext

#overridesRouteSetBuffer: ObjectWithOverrides<Record<string, StudioRouteSet>>
#pendingExpectedPackages: ExpectedPackageDBFromStudioBaselineObjects[] | undefined
#pendingExpectedPlayoutItems: ExpectedPlayoutItemStudio[] | undefined
#routeSetChanged: boolean

constructor(context: JobContext) {
this.#context = context
this.#overridesRouteSetBuffer = { ...context.studio.routeSetsWithOverrides } as ObjectWithOverrides<
Record<string, StudioRouteSet>
>
this.#routeSetChanged = false
}

hasChanges(): boolean {
return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems || this.#routeSetChanged
return !!this.#pendingExpectedPackages || !!this.#pendingExpectedPlayoutItems
}

setExpectedPackages(packages: ExpectedPackageDBFromStudioBaselineObjects[]): void {
Expand Down Expand Up @@ -63,74 +49,9 @@ export class StudioBaselineHelper {
this.#pendingExpectedPackages
)
: undefined,
this.#routeSetChanged
? this.#context.directCollections.Studios.update(
{
_id: this.#context.studioId,
},
{
$set: { 'routeSetsWithOverrides.overrides': this.#overridesRouteSetBuffer.overrides },
}
)
: undefined,
])

this.#pendingExpectedPlayoutItems = undefined
this.#pendingExpectedPackages = undefined
this.#routeSetChanged = false
this.#overridesRouteSetBuffer = { ...this.#context.studio.routeSetsWithOverrides } as ObjectWithOverrides<
Record<string, StudioRouteSet>
>
}

updateRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean {
const studio = this.#context.studio

const routeSets: WrappedOverridableItemNormal<StudioRouteSet>[] = getAllCurrentItemsFromOverrides(
this.#overridesRouteSetBuffer,
null
)

const routeSet = routeSets.find((routeSet) => routeSet.id === routeSetId)

if (routeSet === undefined) throw new Error(`RouteSet "${routeSetId}" not found!`)

if (isActive === 'toggle') isActive = !routeSet.computed.active

if (routeSet.computed?.behavior === StudioRouteBehavior.ACTIVATE_ONLY && isActive === false)
throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`)

const saveOverrides = (newOps: SomeObjectOverrideOp[]) => {
this.#overridesRouteSetBuffer.overrides = newOps
this.#routeSetChanged = true
}
const overrideHelper = new OverrideOpHelperImpl(saveOverrides, this.#overridesRouteSetBuffer)

// Track whether changing this routeset could affect how the timeline is generated, so that it can be following this update
let mayAffectTimeline = couldRoutesetAffectTimelineGeneration(routeSet)

logger.debug(`switchRouteSet "${studio._id}" "${routeSet.id}"=${isActive}`)
overrideHelper.setItemValue(routeSet.id, `active`, isActive)

// Deactivate other routeSets in the same exclusivity group:
if (routeSet.computed.exclusivityGroup && isActive === true) {
for (const [, otherRouteSet] of Object.entries<WrappedOverridableItemNormal<StudioRouteSet>>(routeSets)) {
if (otherRouteSet.id === routeSet.id) continue
if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) {
logger.debug(`switchRouteSet Other ID "${studio._id}" "${otherRouteSet.id}"=false`)
overrideHelper.setItemValue(otherRouteSet.id, `active`, false)

mayAffectTimeline = mayAffectTimeline || couldRoutesetAffectTimelineGeneration(otherRouteSet)
}
}
}

overrideHelper.commit()

return mayAffectTimeline
}
}

function couldRoutesetAffectTimelineGeneration(routeSet: WrappedOverridableItemNormal<StudioRouteSet>): boolean {
return routeSet.computed.abPlayers.length > 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel {
}

switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean {
return this.#baselineHelper.updateRouteSetActive(routeSetId, isActive)
return this.context.setRouteSetActive(routeSetId, isActive)
}

/**
Expand All @@ -125,7 +125,11 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel {
}
this.#timelineHasChanged = false

await this.#baselineHelper.saveAllToDatabase()
await Promise.all([
this.#baselineHelper.saveAllToDatabase(),
this.context.saveRouteSetChanges(),
//
])

if (span) span.end()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/job-worker/src/workers/caches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { clone, deepFreeze } from '@sofie-automation/corelib/dist/lib'
import { logger } from '../logging'
import deepmerge = require('deepmerge')
import { ProcessedShowStyleBase, ProcessedShowStyleVariant, StudioCacheContext } from '../jobs'
import { StudioCacheContextImpl } from './context'
import { StudioCacheContextImpl } from './context/StudioCacheContextImpl'

/**
* A Wrapper to maintain a cache and provide a context using the cache when appropriate
Expand Down
Loading

0 comments on commit d2aa933

Please sign in to comment.