Skip to content

Commit 4a1c882

Browse files
committed
feat: reimplement model switchRouteSet to perform operation on context to simplify persistence and future access of studio
1 parent 191353f commit 4a1c882

File tree

9 files changed

+588
-86
lines changed

9 files changed

+588
-86
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/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 {
@@ -635,6 +635,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
635635
...writePartInstancesAndPieceInstances(this.context, this.allPartInstances),
636636
writeAdlibTestingSegments(this.context, this.rundownsImpl),
637637
this.#baselineHelper.saveAllToDatabase(),
638+
this.context.saveRouteSetChanges(),
638639
])
639640

640641
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/context/JobContextImpl.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ import { TimelineComplete } from '@sofie-automation/corelib/dist/dataModel/Timel
1818
import type { QueueJobFunc } from './util'
1919
import { StudioCacheContextImpl } from './StudioCacheContextImpl'
2020
import { PlaylistLockImpl, RundownLockImpl } from './Locks'
21+
import { StudioRouteSetUpdater } from './StudioRouteSetUpdater'
22+
import type { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
23+
import type { ReadonlyDeep } from 'type-fest'
2124

2225
export class JobContextImpl extends StudioCacheContextImpl implements JobContext {
2326
private readonly locks: Array<LockBase> = []
2427
private readonly caches: Array<BaseModel> = []
2528

29+
private readonly studioRouteSetUpdater: StudioRouteSetUpdater
30+
2631
constructor(
2732
directCollections: Readonly<IDirectCollections>,
2833
cacheData: WorkerDataCache,
@@ -32,6 +37,12 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext
3237
private readonly fastTrackTimeline: FastTrackTimelineFunc | null
3338
) {
3439
super(directCollections, cacheData)
40+
41+
this.studioRouteSetUpdater = new StudioRouteSetUpdater(directCollections, cacheData)
42+
}
43+
44+
get studio(): ReadonlyDeep<DBStudio> {
45+
return this.studioRouteSetUpdater.studioWithChanges ?? super.studio
3546
}
3647

3748
trackCache(cache: BaseModel): void {
@@ -138,4 +149,16 @@ export class JobContextImpl extends StudioCacheContextImpl implements JobContext
138149
})
139150
}
140151
}
152+
153+
setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean {
154+
return this.studioRouteSetUpdater.setRouteSetActive(routeSetId, isActive)
155+
}
156+
157+
async saveRouteSetChanges(): Promise<void> {
158+
return this.studioRouteSetUpdater.saveRouteSetChanges()
159+
}
160+
161+
discardRouteSetChanges(): void {
162+
return this.studioRouteSetUpdater.discardRouteSetChanges()
163+
}
141164
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { StudioRouteBehavior, StudioRouteSet } from '@sofie-automation/blueprints-integration'
2+
import type { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio'
3+
import { deepFreeze } from '@sofie-automation/corelib/dist/lib'
4+
import {
5+
getAllCurrentItemsFromOverrides,
6+
OverrideOpHelperImpl,
7+
WrappedOverridableItemNormal,
8+
} from '@sofie-automation/corelib/dist/overrideOpHelper'
9+
import { logger } from '../../logging'
10+
import type { ReadonlyDeep } from 'type-fest'
11+
import type { WorkerDataCache } from '../caches'
12+
import type { IDirectCollections } from '../../db'
13+
14+
export class StudioRouteSetUpdater {
15+
readonly #directCollections: Readonly<IDirectCollections>
16+
readonly #cacheData: Pick<WorkerDataCache, 'studio'>
17+
18+
constructor(directCollections: Readonly<IDirectCollections>, cacheData: Pick<WorkerDataCache, 'studio'>) {
19+
this.#directCollections = directCollections
20+
this.#cacheData = cacheData
21+
}
22+
23+
// Future: this could store a Map<string, boolean>, if the context exposed a simplified view of DBStudio
24+
#studioWithRouteSetChanges: ReadonlyDeep<DBStudio> | undefined = undefined
25+
26+
get studioWithChanges(): ReadonlyDeep<DBStudio> | undefined {
27+
return this.#studioWithRouteSetChanges
28+
}
29+
30+
setRouteSetActive(routeSetId: string, isActive: boolean | 'toggle'): boolean {
31+
const currentStudio = this.#studioWithRouteSetChanges ?? this.#cacheData.studio
32+
const currentRouteSets = getAllCurrentItemsFromOverrides(currentStudio.routeSetsWithOverrides, null)
33+
34+
const routeSet = currentRouteSets.find((routeSet) => routeSet.id === routeSetId)
35+
if (!routeSet) throw new Error(`RouteSet "${routeSetId}" not found!`)
36+
37+
if (isActive === 'toggle') {
38+
isActive = !routeSet.computed.active
39+
}
40+
41+
if (routeSet.computed.behavior === StudioRouteBehavior.ACTIVATE_ONLY && !isActive)
42+
throw new Error(`RouteSet "${routeSet.id}" is ACTIVATE_ONLY`)
43+
44+
const overrideHelper = new OverrideOpHelperImpl(null, currentStudio.routeSetsWithOverrides)
45+
46+
// Update the pending changes
47+
logger.debug(`switchRouteSet "${this.#cacheData.studio._id}" "${routeSet.id}"=${isActive}`)
48+
overrideHelper.setItemValue(routeSetId, 'active', isActive)
49+
50+
let mayAffectTimeline = couldRoutesetAffectTimelineGeneration(routeSet)
51+
52+
// Deactivate other routeSets in the same exclusivity group:
53+
if (routeSet.computed.exclusivityGroup && isActive) {
54+
for (const otherRouteSet of Object.values<WrappedOverridableItemNormal<StudioRouteSet>>(currentRouteSets)) {
55+
if (otherRouteSet.id === routeSet.id) continue
56+
if (otherRouteSet.computed?.exclusivityGroup === routeSet.computed.exclusivityGroup) {
57+
logger.debug(`switchRouteSet Other ID "${this.#cacheData.studio._id}" "${otherRouteSet.id}"=false`)
58+
overrideHelper.setItemValue(otherRouteSet.id, 'active', false)
59+
60+
mayAffectTimeline = mayAffectTimeline || couldRoutesetAffectTimelineGeneration(otherRouteSet)
61+
}
62+
}
63+
}
64+
65+
const updatedOverrideOps = overrideHelper.getPendingOps()
66+
67+
// Update the cached studio
68+
this.#studioWithRouteSetChanges = Object.freeze({
69+
...currentStudio,
70+
routeSetsWithOverrides: Object.freeze({
71+
...currentStudio.routeSetsWithOverrides,
72+
overrides: deepFreeze(updatedOverrideOps),
73+
}),
74+
})
75+
76+
return mayAffectTimeline
77+
}
78+
79+
async saveRouteSetChanges(): Promise<void> {
80+
if (!this.#studioWithRouteSetChanges) return
81+
82+
// Save the changes to the database
83+
// This is technically a little bit of a race condition, if someone uses the config pages but no more so than the rest of the system
84+
await this.#directCollections.Studios.update(
85+
{
86+
_id: this.#cacheData.studio._id,
87+
},
88+
{
89+
$set: {
90+
'routeSetsWithOverrides.overrides':
91+
this.#studioWithRouteSetChanges.routeSetsWithOverrides.overrides,
92+
},
93+
}
94+
)
95+
96+
// Pretend that the studio as reported by the database has changed, this will be fixed after this job by the ChangeStream firing
97+
this.#cacheData.studio = this.#studioWithRouteSetChanges
98+
this.#studioWithRouteSetChanges = undefined
99+
}
100+
101+
discardRouteSetChanges(): void {
102+
// Discard any pending changes
103+
this.#studioWithRouteSetChanges = undefined
104+
}
105+
}
106+
107+
function couldRoutesetAffectTimelineGeneration(routeSet: WrappedOverridableItemNormal<StudioRouteSet>): boolean {
108+
return routeSet.computed.abPlayers.length > 0
109+
}

0 commit comments

Comments
 (0)