Skip to content

Commit 633dc69

Browse files
committed
Merge branch 'feat/recue-next-part' into bbc-main-autonext
2 parents dc3c908 + 68f568e commit 633dc69

10 files changed

Lines changed: 215 additions & 12 deletions

File tree

packages/blueprints-integration/src/context/playoutActionContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export interface IPlayoutActionContext {
1111
moveNextPart(partDelta: number, segmentDelta: number, ignoreQuickloop?: boolean): Promise<boolean>
1212
/** Set flag to perform a take after the current handler completes. Returns state of the flag after each call. */
1313
takeAfterExecuteAction(take: boolean): Promise<boolean>
14+
/** Restore the next part instance to its original state, then reselect it so onSetAsNext runs again. */
15+
recueNextPart(): Promise<void>
1416
/** Insert a queued part to follow the current part */
1517
queuePart(part: IBlueprintPart, pieces: IBlueprintPiece[]): Promise<IBlueprintPartInstance>
1618
/** Insert a queued part to follow the taken part */

packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/unbound-method */
22
import { IBlueprintMutatablePart, IBlueprintPart, IBlueprintPiece } from '@sofie-automation/blueprints-integration'
3+
import { ActionPartChange } from '../context/services/PartAndPieceInstanceActionService.js'
34
import { ActionExecutionContext } from '../context/adlibActions.js'
45
import { PlayoutModel } from '../../playout/model/PlayoutModel.js'
56
import { WatchedPackagesHelper } from '../context/watchedPackages.js'
@@ -8,6 +9,7 @@ import { mock } from 'jest-mock-extended'
89
import { PartAndPieceInstanceActionService } from '../context/services/PartAndPieceInstanceActionService.js'
910
import { ProcessedShowStyleConfig } from '../config.js'
1011
import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist'
12+
import type { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel.js'
1113

1214
describe('Test blueprint api context', () => {
1315
async function getTestee(rehearsal?: boolean) {
@@ -44,6 +46,16 @@ describe('Test blueprint api context', () => {
4446
}
4547
}
4648

49+
function setupNextPartSnapshot(mockPlayoutModel: PlayoutModel) {
50+
const mockNextPartInstance = mock<PlayoutPartInstanceModel>()
51+
mockNextPartInstance.recueNextPart.mockImplementation(() => undefined)
52+
Object.defineProperty(mockPlayoutModel, 'nextPartInstance', {
53+
get: () => mockNextPartInstance,
54+
})
55+
56+
return { mockNextPartInstance }
57+
}
58+
4759
describe('ActionExecutionContext', () => {
4860
test('getPartInstance', async () => {
4961
const { context, mockActionService } = await getTestee()
@@ -131,6 +143,20 @@ describe('Test blueprint api context', () => {
131143
expect(mockActionService.updatePieceInstance).toHaveBeenCalledWith('pieceId', { name: 'My Piece' })
132144
})
133145

146+
test('recueNextPart restores the next part snapshot and clears next state', async () => {
147+
const { context, mockActionService, mockPlayoutModel } = await getTestee()
148+
const { mockNextPartInstance } = setupNextPartSnapshot(mockPlayoutModel)
149+
150+
mockActionService.nextPartState = ActionPartChange.SAFE_CHANGE
151+
152+
await context.recueNextPart()
153+
154+
expect(mockNextPartInstance.recueNextPart).toHaveBeenCalledTimes(1)
155+
expect(mockActionService.nextPartState).toBe(ActionPartChange.NONE)
156+
expect(context.forceRegenerateTimeline).toBe(true)
157+
expect(context.recueAfterExecute).toBe(true)
158+
})
159+
134160
test('queuePart', async () => {
135161
const { context, mockActionService } = await getTestee()
136162

packages/job-worker/src/blueprints/context/adlibActions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class DatastoreActionExecutionContext
7575
/** Actions */
7676
export class ActionExecutionContext extends ShowStyleUserContext implements IActionExecutionContext, IEventContext {
7777
readonly #tTimersService: TTimersService
78+
public recueAfterExecute = false
7879

7980
/**
8081
* Whether the blueprints requested a take to be performed at the end of this action
@@ -239,6 +240,18 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct
239240
return this.takeAfterExecute
240241
}
241242

243+
async recueNextPart(): Promise<void> {
244+
const nextPartInstance = this._playoutModel.nextPartInstance
245+
if (!nextPartInstance) {
246+
throw new Error('Cannot recue next part when no next part instance is set')
247+
}
248+
249+
nextPartInstance.recueNextPart()
250+
this.partAndPieceInstanceService.nextPartState = ActionPartChange.NONE
251+
this.forceRegenerateTimeline = true
252+
this.recueAfterExecute = true
253+
}
254+
242255
async blockTakeUntil(time: Time | null): Promise<void> {
243256
if (time !== null && (time < getCurrentTime() || typeof time !== 'number'))
244257
throw new Error('Cannot block taking out of the current part, to a time in the past')

packages/job-worker/src/playout/__tests__/playout-executeAction.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import { ActionExecutionContext } from '../../blueprints/context/adlibActions.js
1010
import { ActionPartChange } from '../../blueprints/context/services/PartAndPieceInstanceActionService.js'
1111
import * as Infinites from '../../playout/infinites.js'
1212
import * as TakeApi from '../../playout/take.js'
13+
import * as SetNextApi from '../setNext.js'
1314

1415
const syncPlayheadInfinitesForNextPartInstanceMock = jest.spyOn(Infinites, 'syncPlayheadInfinitesForNextPartInstance')
1516
const takeNextPartMock = jest.spyOn(TakeApi, 'performTakeToNextedPart')
17+
const setNextPartMock = jest.spyOn(SetNextApi, 'setNextPart')
1618

1719
jest.mock('../timeline/generate')
1820
import { updateTimeline } from '../timeline/generate.js'
@@ -44,6 +46,7 @@ describe('Playout API', () => {
4446
syncPlayheadInfinitesForNextPartInstanceMock.mockClear()
4547
updateTimelineMock.mockClear()
4648
takeNextPartMock.mockClear()
49+
setNextPartMock.mockClear()
4750
})
4851

4952
test('throws errors', async () => {
@@ -200,5 +203,27 @@ describe('Playout API', () => {
200203

201204
expect(takeNextPartMock).toHaveBeenCalledTimes(0)
202205
})
206+
207+
test('recue next part reruns setNext', async () => {
208+
setNextPartMock.mockImplementationOnce(async () => Promise.resolve())
209+
210+
context.updateShowStyleBlueprint({
211+
executeAction: async (context) => {
212+
await context.recueNextPart()
213+
},
214+
})
215+
216+
const actionDocId: AdLibActionId = protectString('action-id')
217+
const actionId = 'some-action'
218+
const userData = { blobby: true }
219+
await handleExecuteAdlibAction(context, {
220+
playlistId,
221+
actionDocId,
222+
actionId,
223+
userData,
224+
})
225+
226+
expect(setNextPartMock).toHaveBeenCalledTimes(1)
227+
})
203228
})
204229
})

packages/job-worker/src/playout/adlibAction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
PartAndPieceInstanceActionService,
3434
applyActionSideEffects,
3535
} from '../blueprints/context/services/PartAndPieceInstanceActionService.js'
36+
import { setNextPart } from './setNext.js'
3637
import { convertNoteToNotification } from '../notifications/util.js'
3738
import type { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes'
3839
import { NotificationsModelHelper } from '../notifications/NotificationsModelHelper.js'
@@ -317,6 +318,16 @@ export async function applyAnyExecutionSideEffects(
317318
): Promise<void> {
318319
await applyActionSideEffects(context, playoutModel, actionContext)
319320

321+
if (actionContext.recueAfterExecute) {
322+
actionContext.recueAfterExecute = false
323+
const nextPartInstance = playoutModel.nextPartInstance
324+
if (!nextPartInstance) {
325+
throw new Error('Cannot recue next part when no next part instance is set')
326+
}
327+
328+
await setNextPart(context, playoutModel, nextPartInstance, true)
329+
}
330+
320331
if (actionContext.takeAfterExecute) {
321332
await performTakeToNextedPart(context, playoutModel, now, actionContext.partToQueueAfterTake)
322333
} else if (

packages/job-worker/src/playout/model/PlayoutPartInstanceModel.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export interface PlayoutPartInstanceModel {
4242
*/
4343
snapshotMakeCopy(): PlayoutPartInstanceModelSnapshot
4444

45+
/** Snapshot captured when this part was set as next, used to restore the original nexted state. */
46+
recueNextPartSnapshot?: PlayoutPartInstanceModelSnapshot
47+
48+
/** Restore the previously stored rescue snapshot, if any, and refresh it for future rescues. */
49+
recueNextPart(): void
50+
4551
/**
4652
* Restore a snapshot of this PlayoutPartInstanceModel, to rollback to a previous state
4753
* Note: It is only possible to restore each snapshot once.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
380380
}
381381

382382
clearSelectedPartInstances(): void {
383+
const nextPartInstance = this.nextPartInstance
384+
if (nextPartInstance) nextPartInstance.recueNextPartSnapshot = undefined
383385
this.playlistImpl.currentPartInfo = null
384386
this.playlistImpl.nextPartInfo = null
385387
this.playlistImpl.previousPartInfo = null
@@ -552,6 +554,9 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
552554
}
553555

554556
cycleSelectedPartInstances(): void {
557+
const nextPartInstance = this.nextPartInstance
558+
if (nextPartInstance) nextPartInstance.recueNextPartSnapshot = undefined
559+
555560
this.playlistImpl.previousPartInfo = this.playlistImpl.currentPartInfo
556561
this.playlistImpl.currentPartInfo = this.playlistImpl.nextPartInfo
557562
this.playlistImpl.nextPartInfo = null
@@ -663,6 +668,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
663668
* Reset the playlist for playout
664669
*/
665670
resetPlaylist(regenerateActivationId: boolean): void {
671+
const nextPartInstance = this.nextPartInstance
672+
if (nextPartInstance) nextPartInstance.recueNextPartSnapshot = undefined
666673
this.playlistImpl.previousPartInfo = null
667674
this.playlistImpl.currentPartInfo = null
668675
this.playlistImpl.nextPartInfo = null
@@ -803,6 +810,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
803810
consumesQueuedSegmentId: boolean,
804811
nextTimeOffset?: number
805812
): void {
813+
const previousNextPartInstance = this.nextPartInstance
806814
if (partInstance) {
807815
const storedPartInstance = this.allPartInstances.get(partInstance.partInstance._id)
808816
if (!storedPartInstance) throw new Error(`PartInstance being set as next was not constructed correctly`)
@@ -811,6 +819,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou
811819
throw new Error(`PartInstance being set as next is not current`)
812820
}
813821

822+
if (previousNextPartInstance && previousNextPartInstance.partInstance._id !== partInstance?.partInstance._id) {
823+
previousNextPartInstance.recueNextPartSnapshot = undefined
824+
}
825+
814826
if (partInstance) {
815827
this.playlistImpl.nextPartInfo = literal<SelectedPartInstance>({
816828
partInstanceId: partInstance.partInstance._id,

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

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { PieceId, PieceInstanceId, RundownPlaylistActivationId } from '@sofie-automation/corelib/dist/dataModel/Ids'
1+
import {
2+
PartInstanceId,
3+
PieceId,
4+
PieceInstanceId,
5+
RundownPlaylistActivationId,
6+
} from '@sofie-automation/corelib/dist/dataModel/Ids'
27
import { ReadonlyDeep } from 'type-fest'
38
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
49
import {
@@ -39,9 +44,11 @@ interface PlayoutPieceInstanceModelSnapshotImpl {
3944
PieceInstance: PieceInstance
4045
HasChanges: boolean
4146
}
47+
48+
const recueNextPartSnapshots = new Map<PartInstanceId, PlayoutPartInstanceModelSnapshot>()
49+
4250
class PlayoutPartInstanceModelSnapshotImpl implements PlayoutPartInstanceModelSnapshot {
4351
readonly __isPlayoutPartInstanceModelBackup = true
44-
4552
isRestored = false
4653

4754
readonly partInstance: DBPartInstance
@@ -70,6 +77,21 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel {
7077
partInstanceImpl: DBPartInstance
7178
pieceInstancesImpl: Map<PieceInstanceId, PlayoutPieceInstanceModelImpl | null>
7279
quickLoopService: QuickLoopService
80+
#recueNextPartSnapshot: PlayoutPartInstanceModelSnapshot | undefined
81+
82+
get recueNextPartSnapshot(): PlayoutPartInstanceModelSnapshot | undefined {
83+
return this.#recueNextPartSnapshot
84+
}
85+
86+
set recueNextPartSnapshot(snapshot: PlayoutPartInstanceModelSnapshot | undefined) {
87+
this.#recueNextPartSnapshot = snapshot
88+
const partInstanceId = this.partInstanceImpl?._id
89+
if (partInstanceId) {
90+
if (snapshot) recueNextPartSnapshots.set(partInstanceId, snapshot)
91+
else recueNextPartSnapshots.delete(partInstanceId)
92+
}
93+
this.#partInstanceHasChanges = true
94+
}
7395

7496
#setPartInstanceValue<T extends keyof DBPartInstance>(key: T, newValue: DBPartInstance[T]): void {
7597
if (newValue === undefined) {
@@ -169,6 +191,8 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel {
169191
this.partInstanceImpl = partInstance
170192
this.#partInstanceHasChanges = hasChanges
171193
this.quickLoopService = quickLoopService
194+
const partInstanceId = partInstance?._id
195+
this.#recueNextPartSnapshot = partInstanceId ? recueNextPartSnapshots.get(partInstanceId) : undefined
172196

173197
this.pieceInstancesImpl = new Map()
174198
for (const pieceInstance of pieceInstances) {
@@ -188,29 +212,54 @@ export class PlayoutPartInstanceModelImpl implements PlayoutPartInstanceModel {
188212
return new PlayoutPartInstanceModelSnapshotImpl(this)
189213
}
190214

215+
recueNextPart(): void {
216+
if (!this.recueNextPartSnapshot) {
217+
throw new Error('Cannot recue next part when no snapshot is available')
218+
}
219+
220+
this.snapshotRestore(this.recueNextPartSnapshot)
221+
this.recueNextPartSnapshot = this.snapshotMakeCopy()
222+
}
223+
191224
snapshotRestore(snapshot: PlayoutPartInstanceModelSnapshot): void {
192-
if (!(snapshot instanceof PlayoutPartInstanceModelSnapshotImpl))
225+
if (!snapshot.__isPlayoutPartInstanceModelBackup)
193226
throw new Error(`Cannot restore a Snapshot from an different Model`)
194227

195-
if (snapshot.partInstance._id !== this.partInstance._id)
228+
const snapshotImpl = snapshot as PlayoutPartInstanceModelSnapshotImpl & {
229+
pieceInstances:
230+
| ReadonlyMap<PieceInstanceId, PlayoutPieceInstanceModelSnapshotImpl | null>
231+
| Record<string, PlayoutPieceInstanceModelSnapshotImpl | null>
232+
}
233+
234+
if (snapshotImpl.partInstance._id !== this.partInstance._id)
196235
throw new Error(`Cannot restore a Snapshot from an different PartInstance`)
197236

198-
if (snapshot.isRestored) throw new Error(`Cannot restore a Snapshot which has already been restored`)
199-
snapshot.isRestored = true
237+
if (snapshotImpl.isRestored) throw new Error(`Cannot restore a Snapshot which has already been restored`)
238+
snapshotImpl.isRestored = true
200239

201-
this.partInstanceImpl = snapshot.partInstance
202-
this.#partInstanceHasChanges = snapshot.partInstanceHasChanges
203-
this.pieceInstancesImpl.clear()
204-
for (const [pieceInstanceId, pieceInstance] of snapshot.pieceInstances) {
240+
this.partInstanceImpl = snapshotImpl.partInstance
241+
for (const pieceInstanceId of this.pieceInstancesImpl.keys()) {
242+
this.pieceInstancesImpl.set(pieceInstanceId, null)
243+
}
244+
const pieceInstancesEntries =
245+
snapshotImpl.pieceInstances instanceof Map
246+
? snapshotImpl.pieceInstances.entries()
247+
: Object.entries<PlayoutPieceInstanceModelSnapshotImpl | null>(
248+
snapshotImpl.pieceInstances as Record<string, PlayoutPieceInstanceModelSnapshotImpl | null>
249+
)
250+
251+
for (const [pieceInstanceId, pieceInstance] of pieceInstancesEntries) {
205252
if (pieceInstance) {
206253
this.pieceInstancesImpl.set(
207-
pieceInstanceId,
254+
pieceInstanceId as PieceInstanceId,
208255
new PlayoutPieceInstanceModelImpl(pieceInstance.PieceInstance, pieceInstance.HasChanges)
209256
)
210257
} else {
211-
this.pieceInstancesImpl.set(pieceInstanceId, null)
258+
this.pieceInstancesImpl.set(pieceInstanceId as PieceInstanceId, null)
212259
}
213260
}
261+
262+
this.#partInstanceHasChanges = true
214263
}
215264

216265
blockTakeUntil(timestamp: Time | null): void {

0 commit comments

Comments
 (0)