Skip to content

Commit fc72f02

Browse files
committed
SOFIE-295 | add informative REST results for TAKE failures
1 parent 80fdd49 commit fc72f02

8 files changed

Lines changed: 127 additions & 18 deletions

File tree

meteor/server/api/rest/v1/playlists.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
} from '../../../collections'
3333
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
3434
import { ServerClientAPI } from '../../client'
35-
import { QueueNextSegmentResult, StudioJobs } from '@sofie-automation/corelib/dist/worker/studio'
35+
import { QueueNextSegmentResult, StudioJobs, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio'
3636
import { getCurrentTime } from '../../../lib/lib'
3737
import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api/userActions'
3838
import { ServerRundownAPI } from '../../rundown'
@@ -559,7 +559,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI {
559559
event: string,
560560
rundownPlaylistId: RundownPlaylistId,
561561
fromPartInstanceId: PartInstanceId | undefined
562-
): Promise<ClientAPI.ClientResponse<void>> {
562+
): Promise<ClientAPI.ClientResponse<TakeNextPartResult>> {
563563
triggerWriteAccess()
564564
const playlist = await this.findPlaylist(rundownPlaylistId)
565565

@@ -923,7 +923,7 @@ export function registerRoutes(registerRoute: APIRegisterHook<PlaylistsRestAPI>)
923923
}
924924
)
925925

926-
registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, void>(
926+
registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, TakeNextPartResult>(
927927
'post',
928928
'/playlists/:playlistId/take',
929929
new Map([

meteor/server/lib/rest/v1/playlists.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
RundownPlaylistId,
1212
SegmentId,
1313
} from '@sofie-automation/corelib/dist/dataModel/Ids'
14-
import { QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio'
14+
import { QueueNextSegmentResult, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio'
1515
import { Meteor } from 'meteor/meteor'
1616

1717
/* *************************************************************************
@@ -238,7 +238,7 @@ export interface PlaylistsRestAPI {
238238
event: string,
239239
rundownPlaylistId: RundownPlaylistId,
240240
fromPartInstanceId: PartInstanceId | undefined
241-
): Promise<ClientAPI.ClientResponse<void>>
241+
): Promise<ClientAPI.ClientResponse<TakeNextPartResult>>
242242
/**
243243
* Clears the specified SourceLayers.
244244
*

packages/corelib/src/worker/studio.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,10 @@ export interface CleanupOrphanedExpectedPackageReferencesProps {
380380
rundownId: RundownId
381381
}
382382

383+
export interface TakeNextPartResult {
384+
nextTakeTime: number
385+
}
386+
383387
/**
384388
* Set of valid functions, of form:
385389
* `id: (data) => return`
@@ -404,7 +408,7 @@ export type StudioJobFunc = {
404408
[StudioJobs.QueueNextSegment]: (data: QueueNextSegmentProps) => QueueNextSegmentResult
405409
[StudioJobs.ExecuteAction]: (data: ExecuteActionProps) => ExecuteActionResult
406410
[StudioJobs.ExecuteBucketAdLibOrAction]: (data: ExecuteBucketAdLibOrActionProps) => ExecuteActionResult
407-
[StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => void
411+
[StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => TakeNextPartResult
408412
[StudioJobs.DisableNextPiece]: (data: DisableNextPieceProps) => void
409413
[StudioJobs.RemovePlaylist]: (data: RemovePlaylistProps) => void
410414
[StudioJobs.RegeneratePlaylist]: (data: RegeneratePlaylistProps) => void

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

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ import { PlayoutRundownModel } from './model/PlayoutRundownModel.js'
4040
import { convertNoteToNotification } from '../notifications/util.js'
4141
import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js'
4242

43+
import { TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio'
44+
4345
/**
4446
* Take the currently Next:ed Part (start playing it)
4547
*/
46-
export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise<void> {
48+
export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise<TakeNextPartResult> {
4749
const now = getCurrentTime()
4850

4951
return runJobWithPlayoutModel(
@@ -77,17 +79,29 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart
7779
}
7880
}
7981
if (lastTakeTime && now - lastTakeTime < context.studio.settings.minimumTakeSpan) {
82+
const nextTakeTime = lastTakeTime + context.studio.settings.minimumTakeSpan
8083
logger.debug(
8184
`Time since last take is shorter than ${context.studio.settings.minimumTakeSpan} for ${
8285
playlist.currentPartInfo?.partInstanceId
8386
}: ${now - lastTakeTime}`
8487
)
85-
throw UserError.create(UserErrorMessage.TakeRateLimit, {
86-
duration: context.studio.settings.minimumTakeSpan,
87-
})
88+
throw UserError.create(
89+
UserErrorMessage.TakeRateLimit,
90+
{
91+
duration: context.studio.settings.minimumTakeSpan,
92+
nextAllowedTakeTime: nextTakeTime,
93+
},
94+
429
95+
)
8896
}
8997

90-
return performTakeToNextedPart(context, playoutModel, now, undefined)
98+
const nextTakeTime = now + context.studio.settings.minimumTakeSpan
99+
100+
await performTakeToNextedPart(context, playoutModel, now, undefined)
101+
102+
return {
103+
nextTakeTime,
104+
}
91105
}
92106
)
93107
}
@@ -159,7 +173,14 @@ export async function performTakeToNextedPart(
159173
logger.debug(
160174
`Take is blocked until ${currentPartInstance.partInstance.blockTakeUntil}. Which is in: ${remainingTime}`
161175
)
162-
throw UserError.create(UserErrorMessage.TakeBlockedDuration, { duration: remainingTime })
176+
throw UserError.create(
177+
UserErrorMessage.TakeBlockedDuration,
178+
{
179+
duration: remainingTime,
180+
nextAllowedTakeTime: currentPartInstance.partInstance.blockTakeUntil,
181+
},
182+
425
183+
)
163184
}
164185

165186
// If there was a transition from the previous Part, then ensure that has finished before another take is permitted
@@ -171,11 +192,17 @@ export async function performTakeToNextedPart(
171192
start &&
172193
now < start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration
173194
) {
174-
throw UserError.create(UserErrorMessage.TakeDuringTransition)
195+
throw UserError.create(
196+
UserErrorMessage.TakeDuringTransition,
197+
{
198+
nextAllowedTakeTime: start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration,
199+
},
200+
425
201+
)
175202
}
176203

177204
if (currentPartInstance.isTooCloseToAutonext(true)) {
178-
throw UserError.create(UserErrorMessage.TakeCloseToAutonext)
205+
throw UserError.create(UserErrorMessage.TakeCloseToAutonext, undefined, 425)
179206
}
180207
}
181208

packages/meteor-lib/src/api/__tests__/client.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ describe('ClientAPI', () => {
5050
})
5151
}
5252
})
53+
it('Extracts nextAllowedTakeTime from error args', () => {
54+
const error = ClientAPI.responseError(
55+
UserError.create(
56+
UserErrorMessage.TakeRateLimit,
57+
{
58+
duration: 1000,
59+
nextAllowedTakeTime: 1234567890,
60+
},
61+
429
62+
)
63+
)
64+
expect(error.nextAllowedTakeTime).toBe(1234567890)
65+
expect(error.errorCode).toBe(429)
66+
})
67+
it('Does not include nextAllowedTakeTime when not in args', () => {
68+
const error = ClientAPI.responseError(UserError.create(UserErrorMessage.InactiveRundown))
69+
expect(error.nextAllowedTakeTime).toBeUndefined()
70+
})
5371
describe('isClientResponseSuccess', () => {
5472
it('Correctly recognizes a responseSuccess object', () => {
5573
const response = ClientAPI.responseSuccess(undefined)

packages/meteor-lib/src/api/client.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export namespace ClientAPI {
5050
errorCode: number
5151
/** On error, provide a human-readable error message */
5252
error: SerializedUserError
53+
/** For blocked TAKE operations, the next allowed take time (Unix timestamp ms) */
54+
nextAllowedTakeTime?: number
5355
}
5456

5557
/**
@@ -59,7 +61,12 @@ export namespace ClientAPI {
5961
* @returns A `ClientResponseError` object containing the error and the resolved error code.
6062
*/
6163
export function responseError(userError: UserError): ClientResponseError {
62-
return { error: UserError.serialize(userError), errorCode: userError.errorCode }
64+
const nextAllowedTakeTime = userError.userMessage.args?.nextAllowedTakeTime as number | undefined
65+
return {
66+
error: UserError.serialize(userError),
67+
errorCode: userError.errorCode,
68+
...(nextAllowedTakeTime !== undefined && { nextAllowedTakeTime }),
69+
}
6370
}
6471
export interface ClientResponseSuccess<Result> {
6572
/** On success, return success code (by default, use 200) */

packages/meteor-lib/src/api/userActions.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLi
66
import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction'
77
import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction'
88
import { Time } from '@sofie-automation/blueprints-integration'
9-
import { ExecuteActionResult, QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio'
9+
import {
10+
ExecuteActionResult,
11+
QueueNextSegmentResult,
12+
TakeNextPartResult,
13+
} from '@sofie-automation/corelib/dist/worker/studio'
1014
import {
1115
AdLibActionId,
1216
BucketAdLibActionId,
@@ -34,7 +38,7 @@ export interface NewUserActionAPI {
3438
eventTime: Time,
3539
rundownPlaylistId: RundownPlaylistId,
3640
fromPartInstanceId: PartInstanceId | null
37-
): Promise<ClientAPI.ClientResponse<void>>
41+
): Promise<ClientAPI.ClientResponse<TakeNextPartResult>>
3842
setNext(
3943
userEvent: string,
4044
eventTime: Time,

packages/openapi/api/definitions/playlists.yaml

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,22 @@ resources:
589589
description: May be specified to ensure that multiple take requests from the same Part do not result in multiple takes.
590590
responses:
591591
200:
592-
$ref: '#/components/responses/putSuccess'
592+
description: Take was successful - returns the next allowed take time.
593+
content:
594+
application/json:
595+
schema:
596+
type: object
597+
properties:
598+
status:
599+
type: number
600+
example: 200
601+
result:
602+
type: object
603+
properties:
604+
nextTakeTime:
605+
type: number
606+
description: Unix timestamp (ms) of when the next take will be allowed.
607+
example: 1707024000000
593608
404:
594609
$ref: '#/components/responses/playlistNotFound'
595610
412:
@@ -605,6 +620,40 @@ resources:
605620
message:
606621
type: string
607622
example: No Next point found, please set a part as Next before doing a TAKE.
623+
425:
624+
description: Take is blocked due to a transition or adlib action.
625+
content:
626+
application/json:
627+
schema:
628+
type: object
629+
properties:
630+
status:
631+
type: number
632+
example: 425
633+
message:
634+
type: string
635+
example: Cannot take during a transition
636+
nextAllowedTakeTime:
637+
type: number
638+
description: Unix timestamp (ms) of when the next take will be allowed.
639+
example: 1707024000000
640+
429:
641+
description: Take rate limit exceeded - takes are happening too quickly.
642+
content:
643+
application/json:
644+
schema:
645+
type: object
646+
properties:
647+
status:
648+
type: number
649+
example: 429
650+
message:
651+
type: string
652+
example: Ignoring TAKES that are too quick after eachother (1000 ms)
653+
nextAllowedTakeTime:
654+
type: number
655+
description: Unix timestamp (ms) of when the next take will be allowed.
656+
example: 1707024000000
608657
500:
609658
$ref: '#/components/responses/internalServerError'
610659

0 commit comments

Comments
 (0)