Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
395a90c
feat: Set here as next
olzzon Feb 11, 2025
c741c3e
chore: add missing type
Feb 14, 2025
3363e86
feat: calculate lookahead offsets when setting a take point in the mi…
justandras Jan 22, 2026
bd80b43
chore: improve type safety for lookaheadOffset calculations, ignore o…
justandras Dec 3, 2025
4045189
fix: ignore pieces that are replaced before the in-point by another p…
justandras Jan 22, 2026
ac7c2e9
chore: fix linting issues
justandras Dec 5, 2025
4fd80ad
fix: sorting of timelineObjects
justandras Dec 10, 2025
9f474a3
chore: refactor and update findForLayer tests
justandras Dec 10, 2025
5424645
chore: add tests for lookaheadOffset
justandras Dec 16, 2025
a761dc2
chore: remove console logs
justandras Dec 22, 2025
52515ba
chore: add test case for single layer parts with while timings
justandras Dec 22, 2025
151b4be
chore: move constants into separate file
justandras Dec 22, 2025
e43669c
chore: update tests to conform to already existing tests
justandras Dec 22, 2025
74eb8e4
fix: correctly update the timeline offset
justandras Dec 30, 2025
0f0f430
fix: Allow upcoming part instances to be nexted to override nextTimeO…
justandras Jan 19, 2026
43ce2eb
fix: display correct timestamps
justandras Jan 20, 2026
985474f
feat: reset and create new pieceInstance if an already played instanc…
justandras Jan 22, 2026
0306d1b
fix: block reuse of partInstances from the UI
justandras Jan 22, 2026
4742ace
fix: do not reset past instances, rather fully reuse them where possible
justandras Jan 22, 2026
ef25d2c
fix: block play/set from here for current part if playhead is past th…
justandras Jan 23, 2026
d396840
fix: block current part from being nexted when orphaned
justandras Jan 23, 2026
9de4efe
chore: update TSR dependencies
justandras Jan 23, 2026
a859b22
chore: update tests due to changes from time of day pieces
justandras Jan 23, 2026
498e489
chore: fix package.json
justandras Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions meteor/server/api/userActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest'
import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions'
import { assertConnectionHasOneOfPermissions } from '../security/auth'
import { checkAccessToRundown } from '../security/check'
import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'

const PERMISSIONS_FOR_PLAYOUT_USERACTION: Array<keyof UserPermissions> = ['studio']
const PERMISSIONS_FOR_BUCKET_MODIFICATION: Array<keyof UserPermissions> = ['studio']
Expand Down Expand Up @@ -121,8 +122,9 @@ class ServerUserActionAPI
userEvent: string,
eventTime: Time,
rundownPlaylistId: RundownPlaylistId,
nextPartId: PartId,
timeOffset: number | null
nextPartOrInstanceId: PartId | PartInstanceId,
timeOffset: number | null,
isInstance: boolean | null
) {
return ServerClientAPI.runUserActionInLogForPlaylistOnWorker(
this,
Expand All @@ -131,12 +133,15 @@ class ServerUserActionAPI
rundownPlaylistId,
() => {
check(rundownPlaylistId, String)
check(nextPartId, String)
check(nextPartOrInstanceId, String)
},
Comment on lines 134 to 137
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing validation for timeOffset and isInstance parameters.

The validation callback only checks rundownPlaylistId and nextPartOrInstanceId, but omits timeOffset and isInstance. Every other method in this class validates all incoming parameters (e.g., take validates fromPartInstanceId with Match.OneOf). Without these checks, malformed inputs bypass Meteor's type enforcement.

Proposed fix
 () => {
 	check(rundownPlaylistId, String)
 	check(nextPartOrInstanceId, String)
+	check(timeOffset, Match.OneOf(Number, null))
+	check(isInstance, Match.OneOf(Boolean, null))
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
() => {
check(rundownPlaylistId, String)
check(nextPartId, String)
check(nextPartOrInstanceId, String)
},
() => {
check(rundownPlaylistId, String)
check(nextPartOrInstanceId, String)
check(timeOffset, Match.OneOf(Number, null))
check(isInstance, Match.OneOf(Boolean, null))
},
🤖 Prompt for AI Agents
In `@meteor/server/api/userActions.ts` around lines 134 - 137, The
parameter-validation callback in userActions.ts currently only checks
rundownPlaylistId and nextPartOrInstanceId but skips timeOffset and isInstance;
update that same validation callback to assert timeOffset and isInstance too
(use Match.OneOf(Number, null) or Match.Optional(Number) for timeOffset if it
can be null/optional, and check(isInstance, Boolean)) so all four parameters
(rundownPlaylistId, nextPartOrInstanceId, timeOffset, isInstance) are validated
consistent with other methods like take (which uses Match.OneOf for
fromPartInstanceId).

StudioJobs.SetNextPart,
{
playlistId: rundownPlaylistId,
nextPartId,
nextPartId: isInstance ? undefined : protectString<PartId>(unprotectString(nextPartOrInstanceId)),
nextPartInstanceId: isInstance
? protectString<PartInstanceId>(unprotectString(nextPartOrInstanceId))
: undefined,
setManually: true,
nextTimeOffset: timeOffset ?? undefined,
}
Expand Down
10 changes: 5 additions & 5 deletions meteor/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1411,7 +1411,7 @@ __metadata:
dependencies:
"@mos-connection/model": "npm:^5.0.0-alpha.0"
kairos-lib: "npm:^0.2.3"
timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0"
timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20260123-151128-04b075e87.0"
tslib: "npm:^2.8.1"
type-fest: "npm:^4.41.0"
languageName: node
Expand Down Expand Up @@ -9831,12 +9831,12 @@ __metadata:
languageName: node
linkType: hard

"timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0":
version: 10.0.0-nightly-release53-20251217-143607-df590aa96.0
resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0"
"timeline-state-resolver-types@npm:10.0.0-nightly-release53-20260123-151128-04b075e87.0":
version: 10.0.0-nightly-release53-20260123-151128-04b075e87.0
resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release53-20260123-151128-04b075e87.0"
dependencies:
tslib: "npm:^2.8.1"
checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf
checksum: 10/7322e958a35bc9deaa332c201414248b097c5894b556e481c5a97d51e292cb61c017d3d373fe1ae3bf19383965535dbfe104d8c38ca7c109eadbffc781a08ed9
languageName: node
linkType: hard

Expand Down
3 changes: 2 additions & 1 deletion packages/corelib/src/worker/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ export interface ActivateRundownPlaylistProps extends RundownPlayoutPropsBase {
}
export type DeactivateRundownPlaylistProps = RundownPlayoutPropsBase
export interface SetNextPartProps extends RundownPlayoutPropsBase {
nextPartId: PartId
nextPartId?: PartId
nextPartInstanceId?: PartInstanceId
Comment on lines +257 to +258
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce exactly one next identifier in SetNextPartProps.

Both fields being optional lets callers omit both or set both, which can silently produce invalid jobs. Suggest a union that requires exactly one.

✅ Proposed type guard
-export interface SetNextPartProps extends RundownPlayoutPropsBase {
-	nextPartId?: PartId
-	nextPartInstanceId?: PartInstanceId
-	setManually?: boolean
-	nextTimeOffset?: number
-}
+export type SetNextPartProps = RundownPlayoutPropsBase &
+	(
+		| { nextPartId: PartId; nextPartInstanceId?: never }
+		| { nextPartInstanceId: PartInstanceId; nextPartId?: never }
+	) & {
+		setManually?: boolean
+		nextTimeOffset?: number
+	}
🤖 Prompt for AI Agents
In `@packages/corelib/src/worker/studio.ts` around lines 257 - 258,
SetNextPartProps currently allows both nextPartId and nextPartInstanceId to be
omitted or both set; change it to a union that enforces exactly one identifier
is present by replacing the optional-pair with a discriminated XOR: one branch
with nextPartId: PartId and nextPartInstanceId?: never, and the other with
nextPartInstanceId: PartInstanceId and nextPartId?: never; update any callers
and add a small type guard function (e.g., isNextPartId(props): props is {
nextPartId: PartId; nextPartInstanceId?: never } ) to help narrow the union
where needed.

setManually?: boolean
nextTimeOffset?: number
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
jest.mock('../../findObjects')
import { context, TfindLookaheadObjectsForPart } from './helpers/mockSetup.js'
import { findLookaheadForLayer } from '../../findForLayer.js'
import { expectInstancesToMatch } from '../utils.js'
import { findForLayerTestConstants } from './constants.js'
import { findLookaheadObjectsForPart } from '../../findObjects.js'

const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart
const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock

beforeEach(() => {
findLookaheadObjectsForPartMock.mockReset()
})

const current = findForLayerTestConstants.current
const nextFuture = findForLayerTestConstants.nextFuture
const layer = findForLayerTestConstants.layer

describe('findLookaheadForLayer – basic behavior', () => {
test('no parts', () => {
const res = findLookaheadForLayer(context, {}, [], 'abc', 1, 1)

expect(res.timed).toHaveLength(0)
expect(res.future).toHaveLength(0)
})
test('if the previous part is unset', () => {
findLookaheadObjectsForPartMock.mockReturnValue([])

findLookaheadForLayer(context, { previous: undefined, current, next: nextFuture }, [], layer, 1, 1)

expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2)
expectInstancesToMatch(findLookaheadObjectsForPartMock, 1, layer, current, undefined)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { getRandomString } from '@sofie-automation/corelib/dist/lib'
import { PartInstanceAndPieceInstances, PartAndPieces } from '../../util.js'
import { createFakePiece } from '../utils.js'

const layer: string = getRandomString()

export const findForLayerTestConstants = {
previous: {
part: { _id: 'pPrev', part: 'prev' },
allPieces: [createFakePiece('1'), createFakePiece('2'), createFakePiece('3')],
onTimeline: true,
nowInPart: 2000,
} as any as PartInstanceAndPieceInstances,
current: {
part: { _id: 'pCur', part: 'cur' },
allPieces: [createFakePiece('4'), createFakePiece('5'), createFakePiece('6')],
onTimeline: true,
nowInPart: 1000,
} as any as PartInstanceAndPieceInstances,
nextTimed: {
part: { _id: 'pNextTimed', part: 'nextT' },
allPieces: [createFakePiece('7'), createFakePiece('8'), createFakePiece('9')],
onTimeline: true,
} as any as PartInstanceAndPieceInstances,
nextFuture: {
part: { _id: 'pNextFuture', part: 'nextF' },
allPieces: [createFakePiece('10'), createFakePiece('11'), createFakePiece('12')],
onTimeline: false,
} as any as PartInstanceAndPieceInstances,

orderedParts: [{ _id: 'p1' }, { _id: 'p2', invalid: true }, { _id: 'p3' }, { _id: 'p4' }, { _id: 'p5' }].map(
(p) => ({
part: p as any,
usesInTransition: true,
pieces: [{ _id: p._id + '_p1' } as any],
})
) as PartAndPieces[],

layer,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { setupDefaultJobEnvironment } from '../../../../../__mocks__/context.js'
import { findLookaheadObjectsForPart } from '../../../../../playout/lookahead/findObjects.js'

export type TfindLookaheadObjectsForPart = jest.MockedFunction<typeof findLookaheadObjectsForPart>

export const context = setupDefaultJobEnvironment()
Loading
Loading