Skip to content

Commit 0e7c55a

Browse files
authored
Merge pull request #1621 from bbc/feat/SOFIE-295
feat: add informative REST results for TAKE failures (SOFIE-295)
2 parents 0c60e8a + eacf9aa commit 0e7c55a

File tree

9 files changed

+132
-19
lines changed

9 files changed

+132
-19
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ interface APIRequestError {
117117
status: number
118118
message: string
119119
details?: string[]
120+
additionalInfo?: Record<string, unknown>
120121
}
121122

122123
function sofieAPIRequest<API, Params, Body, Response>(
@@ -133,6 +134,7 @@ function sofieAPIRequest<API, Params, Body, Response>(
133134
) => Promise<ClientAPI.ClientResponse<Response>>
134135
) {
135136
koaRouter[method](route, async (ctx, next) => {
137+
let responseAdditionalInfo: Record<string, unknown> | undefined
136138
try {
137139
const context = new APIContext()
138140
const serverAPI = serverAPIFactory.createServerAPI(context)
@@ -144,6 +146,7 @@ function sofieAPIRequest<API, Params, Body, Response>(
144146
ctx.request.body as unknown as Body
145147
)
146148
if (ClientAPI.isClientResponseError(response)) {
149+
responseAdditionalInfo = response.additionalInfo
147150
throw UserError.fromSerialized(response.error)
148151
}
149152
ctx.body = JSON.stringify({ status: response.success, result: response.result })
@@ -176,7 +179,8 @@ function sofieAPIRequest<API, Params, Body, Response>(
176179
ctx.type = 'application/json'
177180
const bodyObj: APIRequestError = { status: errCode, message: errMsg }
178181
const details = extractErrorDetails(e)
179-
if (details) bodyObj['details'] = details
182+
if (details) bodyObj.details = details
183+
if (responseAdditionalInfo) bodyObj.additionalInfo = responseAdditionalInfo
180184
ctx.body = JSON.stringify(bodyObj)
181185
ctx.status = errCode
182186
}

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
@@ -381,6 +381,10 @@ export interface CleanupOrphanedExpectedPackageReferencesProps {
381381
rundownId: RundownId
382382
}
383383

384+
export interface TakeNextPartResult {
385+
nextTakeTime: number
386+
}
387+
384388
/**
385389
* Set of valid functions, of form:
386390
* `id: (data) => return`
@@ -405,7 +409,7 @@ export type StudioJobFunc = {
405409
[StudioJobs.QueueNextSegment]: (data: QueueNextSegmentProps) => QueueNextSegmentResult
406410
[StudioJobs.ExecuteAction]: (data: ExecuteActionProps) => ExecuteActionResult
407411
[StudioJobs.ExecuteBucketAdLibOrAction]: (data: ExecuteBucketAdLibOrActionProps) => ExecuteActionResult
408-
[StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => void
412+
[StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => TakeNextPartResult
409413
[StudioJobs.DisableNextPiece]: (data: DisableNextPieceProps) => void
410414
[StudioJobs.RemovePlaylist]: (data: RemovePlaylistProps) => void
411415
[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 additionalInfo 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.additionalInfo).toEqual({ duration: 1000, nextAllowedTakeTime: 1234567890 })
65+
expect(error.errorCode).toBe(429)
66+
})
67+
it('Does not include additionalInfo when no args', () => {
68+
const error = ClientAPI.responseError(UserError.create(UserErrorMessage.InactiveRundown))
69+
expect(error.additionalInfo).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+
/** Optional additional information about the error, forwarded from UserError args */
54+
additionalInfo?: Record<string, unknown>
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 args = userError.userMessage.args
65+
return {
66+
error: UserError.serialize(userError),
67+
errorCode: userError.errorCode,
68+
...(args !== undefined && Object.keys(args).length > 0 && { additionalInfo: args }),
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+
additionalInfo:
637+
type: object
638+
description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes.
639+
additionalProperties: true
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+
additionalInfo:
654+
type: object
655+
description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes.
656+
additionalProperties: true
608657
500:
609658
$ref: '#/components/responses/internalServerError'
610659

0 commit comments

Comments
 (0)