Skip to content

Commit dba153c

Browse files
authored
Merge pull request Sofie-Automation#1754 from bbc/feat/hook-on-snapshot-creation
feat: blueprint hooks on snapshot creation
2 parents d94a5d8 + 25311cb commit dba153c

16 files changed

Lines changed: 865 additions & 23 deletions

File tree

meteor/server/api/snapshot.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { CURRENT_SYSTEM_VERSION } from '../migration/currentSystemVersion'
4040
import { isVersionSupported } from '../migration/databaseMigration'
4141
import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant'
4242
import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint'
43-
import { IngestRundown, VTContent } from '@sofie-automation/blueprints-integration'
43+
import { BlueprintSnapshotType, IngestRundown, VTContent } from '@sofie-automation/blueprints-integration'
4444
import { MongoQuery } from '@sofie-automation/corelib/dist/mongo'
4545
import { importIngestRundown } from './ingest/http'
4646
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/RundownPlaylist'
@@ -327,7 +327,8 @@ function getPiecesMediaObjects(pieces: PieceGeneric[]): string[] {
327327

328328
async function createRundownPlaylistSnapshot(
329329
playlist: VerifiedRundownPlaylistForUserAction,
330-
options: PlaylistSnapshotOptions
330+
options: PlaylistSnapshotOptions,
331+
reason?: string
331332
): Promise<RundownPlaylistSnapshot> {
332333
/** Max count of one type of items to include in the snapshot */
333334
const LIMIT_COUNT = 500
@@ -343,6 +344,8 @@ async function createRundownPlaylistSnapshot(
343344
playlistId: playlist._id,
344345
full: !!options.withArchivedDocuments,
345346
withTimeline: !!options.withTimeline,
347+
snapshotId,
348+
reason,
346349
})
347350
const coreResult = await queuedJob.complete
348351
const coreSnapshot: CoreRundownPlaylistSnapshot = JSONBlobParse(coreResult.snapshotJson)
@@ -693,27 +696,71 @@ export async function storeSystemSnapshot(
693696

694697
return internalStoreSystemSnapshot(options, reason)
695698
}
699+
/**
700+
* Runs {@link StudioJobs.OnSystemSnapshotCreated} for each studio after a snapshot is stored.
701+
*
702+
* Studio-scoped system snapshots run one job; full-system snapshots run one job per studio in the snapshot.
703+
* Waits for each blueprint hook to finish; hook failures are logged and do not fail snapshot storage.
704+
*/
705+
async function queueOnSystemSnapshotCreatedJobs(
706+
storedId: SnapshotId,
707+
reason: string,
708+
type: BlueprintSnapshotType,
709+
options: SystemSnapshotOptions,
710+
studioIds: StudioId[]
711+
): Promise<void> {
712+
const fullSystem = !options.studioId
713+
714+
for (const studioId of studioIds) {
715+
try {
716+
const job = await QueueStudioJob(StudioJobs.OnSystemSnapshotCreated, studioId, {
717+
snapshotId: storedId,
718+
reason,
719+
type,
720+
options: {
721+
studioId,
722+
withDeviceSnapshots: options.withDeviceSnapshots,
723+
fullSystem,
724+
},
725+
})
726+
await job.complete
727+
} catch (err) {
728+
logger.error(
729+
`OnSystemSnapshotCreated failed for snapshot ${storedId} (studio ${studioId}, withDeviceSnapshots=${options.withDeviceSnapshots}): ${stringifyError(err)}`
730+
)
731+
}
732+
}
733+
}
734+
696735
/** Take and store a system snapshot. For internal use only, performs no access control. */
697736
export async function internalStoreSystemSnapshot(options: SystemSnapshotOptions, reason: string): Promise<SnapshotId> {
698737
check(options.studioId, Match.Optional(String))
699738

700739
const s = await createSystemSnapshot(options)
701-
return storeSnaphot(s, reason)
740+
const storedId = await storeSnaphot(s, reason)
741+
742+
// Full-system snapshots: one blueprint hook job per studio in the snapshot
743+
const studioIds = options.studioId ? [options.studioId] : s.studios.map((studio) => studio._id)
744+
if (studioIds.length > 0) {
745+
await queueOnSystemSnapshotCreatedJobs(storedId, reason, 'system', options, studioIds)
746+
}
747+
748+
return storedId
702749
}
703750
export async function storeRundownPlaylistSnapshot(
704751
playlist: VerifiedRundownPlaylistForUserAction,
705752
options: PlaylistSnapshotOptions,
706753
reason: string
707754
): Promise<SnapshotId> {
708-
const s = await createRundownPlaylistSnapshot(playlist, options)
755+
const s = await createRundownPlaylistSnapshot(playlist, options, reason)
709756
return storeSnaphot(s, reason)
710757
}
711758
export async function internalStoreRundownPlaylistSnapshot(
712759
playlist: DBRundownPlaylist,
713760
options: PlaylistSnapshotOptions,
714761
reason: string
715762
): Promise<SnapshotId> {
716-
const s = await createRundownPlaylistSnapshot(playlist, options)
763+
const s = await createRundownPlaylistSnapshot(playlist, options, reason)
717764
return storeSnaphot(s, reason)
718765
}
719766
export async function storeDebugSnapshot(
@@ -731,7 +778,13 @@ export async function storeDebugSnapshot(
731778
assertConnectionHasOneOfPermissions(context.connection, ...PERMISSIONS_FOR_SNAPSHOT_MANAGEMENT)
732779

733780
const s = await createDebugSnapshot(studioId)
734-
return storeSnaphot(s, reason)
781+
const storedId = await storeSnaphot(s, reason)
782+
783+
await queueOnSystemSnapshotCreatedJobs(storedId, reason, 'debug', { studioId, withDeviceSnapshots: true }, [
784+
studioId,
785+
])
786+
787+
return storedId
735788
}
736789
export async function restoreSnapshot(
737790
context: MethodContext,

packages/blueprints-integration/src/api/showStyle.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type {
1818
IFixUpConfigContext,
1919
IOnTakeContext,
2020
IOnSetAsNextContext,
21+
IPlaylistSnapshotCreatedContext,
22+
IBlueprintPlaylistSnapshotInfo,
2123
} from '../context/index.js'
2224
import type { IngestAdlib, ExtendedIngestRundown, IngestRundown } from '../ingest.js'
2325
import type { IBlueprintExternalMessageQueueObj } from '../message.js'
@@ -201,6 +203,25 @@ export interface ShowStyleBlueprintManifest<
201203

202204
// Events
203205

206+
/**
207+
* Called after a rundown playlist snapshot has been generated, before Meteor persists the snapshot file.
208+
*
209+
* Use this to run show-specific side effects (e.g. TSR actions) when a playlist snapshot is taken.
210+
* The callback receives {@link IPlaylistSnapshotCreatedContext} with `listPlayoutDevices` and `executeTSRAction`.
211+
*
212+
* For playlists containing multiple rundowns, only one show-style blueprint is invoked per snapshot:
213+
* current part, then next part, then first rundown by name.
214+
*
215+
* Errors are logged by Core and do not fail snapshot generation or storage.
216+
*
217+
* @param context Show-style and studio context with TSR actions for the studio worker job.
218+
* @param info Metadata about the snapshot (not the snapshot JSON).
219+
*/
220+
onPlaylistSnapshotCreated?: (
221+
context: IPlaylistSnapshotCreatedContext,
222+
info: IBlueprintPlaylistSnapshotInfo
223+
) => Promise<void>
224+
204225
/**
205226
* Called at the final stage of RundownPlaylist activation, before the updated timeline is submitted to the Playout Gateway,
206227
* This is a good place to prepare any external systems for the rundown going live.

packages/blueprints-integration/src/api/studio.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import type {
99
IStudioBaselineContext,
1010
IStudioUserContext,
1111
IProcessIngestDataContext,
12+
ISystemSnapshotCreatedContext,
13+
IBlueprintSystemSnapshotInfo,
14+
BlueprintSnapshotType as _BlueprintSnapshotType,
1215
} from '../context/index.js'
1316
import type { IBlueprintShowStyleBase } from '../showStyle.js'
1417
import type {
@@ -193,6 +196,27 @@ export interface StudioBlueprintManifest<
193196
previousNrcsIngestRundown: IngestRundown | undefined,
194197
changes: NrcsIngestChangeDetails | UserOperationChange | PlayoutOperationChange
195198
) => Promise<void>
199+
200+
/**
201+
* Called after a system snapshot has been stored to disk.
202+
*
203+
* Use this to run studio-level side effects (e.g. TSR actions on playout devices) at snapshot time.
204+
* The callback receives {@link ISystemSnapshotCreatedContext} with `listPlayoutDevices` and `executeTSRAction`.
205+
*
206+
* Invoked once per studio:
207+
* - Studio-scoped snapshots (`studioId` in snapshot options): once for that studio.
208+
* - Full-system snapshots (no `studioId`): once per studio included in the snapshot.
209+
* - Debug snapshots: once for the target studio when `info.type` is `'debug'` ({@link _BlueprintSnapshotType}).
210+
*
211+
* Errors are logged by Core and do not fail snapshot storage.
212+
*
213+
* @param context Studio context and TSR actions for the studio worker job.
214+
* @param info Metadata about the snapshot (not the snapshot JSON).
215+
*/
216+
onSystemSnapshotCreated?: (
217+
context: ISystemSnapshotCreatedContext,
218+
info: IBlueprintSystemSnapshotInfo
219+
) => Promise<void>
196220
}
197221

198222
export interface BlueprintResultStudioBaseline {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { PeripheralDeviceId } from '@sofie-automation/shared-lib/dist/core/model
22
import { IBlueprintPlayoutDevice, TSR } from '../index.js'
33

44
export interface IExecuteTSRActionsContext {
5-
/** Returns a list of the PeripheralDevices */
5+
/** Returns playout-gateway subdevices for the studio, or an empty list if none are configured. */
66
listPlayoutDevices(): Promise<IBlueprintPlayoutDevice[]>
77
/** Execute an action on a certain PeripheralDevice */
88
executeTSRAction(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './playoutStore.js'
99
export * from './processIngestDataContext.js'
1010
export * from './rundownContext.js'
1111
export * from './showStyleContext.js'
12+
export * from './snapshotContext.js'
1213
export * from './studioContext.js'
1314
export * from './syncIngestChangesContext.js'
1415
export * from './tTimersContext.js'
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { IExecuteTSRActionsContext } from './executeTsrActionContext.js'
2+
import type { IShowStyleContext } from './showStyleContext.js'
3+
import type { IStudioContext } from './studioContext.js'
4+
5+
/**
6+
* How the system snapshot hook was triggered.
7+
* - `system` — system snapshot (settings, REST, automatic snapshot before migration)
8+
* - `debug` — debug snapshot capture from the rundown UI
9+
*/
10+
export type BlueprintSnapshotType = 'system' | 'debug'
11+
12+
/** Options that were used when the system snapshot was created. */
13+
export interface IBlueprintSystemSnapshotOptions {
14+
/** Studio this hook invocation runs for (same as context; use `fullSystem` for snapshot scope). */
15+
studioId?: string
16+
/** Whether peripheral device state snapshots were included in the stored snapshot file. */
17+
withDeviceSnapshots?: boolean
18+
/**
19+
* `true` when the stored snapshot is a full-system snapshot (all studios).
20+
* `false` when the snapshot was filtered to a single studio.
21+
*/
22+
fullSystem?: boolean
23+
}
24+
25+
/**
26+
* Metadata passed to {@link StudioBlueprintManifest.onSystemSnapshotCreated}.
27+
* Does not include the snapshot file contents.
28+
*/
29+
export interface IBlueprintSystemSnapshotInfo {
30+
/** Id of the stored snapshot (same value for every studio when `fullSystem` is true). */
31+
snapshotId: string
32+
/** Human-readable reason provided when the snapshot was requested (e.g. UI label, cron message). */
33+
reason: string
34+
type: BlueprintSnapshotType
35+
/** Snapshot creation options relevant to this hook invocation. */
36+
options: IBlueprintSystemSnapshotOptions
37+
}
38+
39+
/**
40+
* Context for {@link StudioBlueprintManifest.onSystemSnapshotCreated}.
41+
* Provides studio config, mappings, and TSR device actions for the studio worker job.
42+
*/
43+
export interface ISystemSnapshotCreatedContext extends IStudioContext, IExecuteTSRActionsContext {}
44+
45+
/** Options that were used when the playlist snapshot was generated. */
46+
export interface IBlueprintPlaylistSnapshotOptions {
47+
/** When true, all part/piece instances are included; when false, only recent/non-reset instances. */
48+
full: boolean
49+
/** When true and the playlist is activated, the timeline is included in the snapshot data. */
50+
withTimeline: boolean
51+
}
52+
53+
/** Minimal playlist state exposed to the blueprint (not the full DB document). */
54+
export interface IBlueprintPlaylistSnapshotPlaylistInfo {
55+
/** Playlist display name. */
56+
name: string
57+
/** Whether the playlist had an active activation at snapshot time. */
58+
active: boolean
59+
/** Whether the playlist was in rehearsal mode at snapshot time. */
60+
rehearsal: boolean
61+
}
62+
63+
/**
64+
* Metadata passed to {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated}.
65+
* Does not include the snapshot file contents.
66+
*/
67+
export interface IBlueprintPlaylistSnapshotInfo {
68+
/** Id assigned to this snapshot before generation completes. */
69+
snapshotId: string
70+
/** Id of the rundown playlist that was snapshotted. */
71+
playlistId: string
72+
/** Human-readable reason provided when the snapshot was requested. */
73+
reason: string
74+
/** Snapshot generation options. */
75+
options: IBlueprintPlaylistSnapshotOptions
76+
/** Summary of playlist state at snapshot time. */
77+
playlist: IBlueprintPlaylistSnapshotPlaylistInfo
78+
}
79+
80+
/**
81+
* Context for {@link ShowStyleBlueprintManifest.onPlaylistSnapshotCreated}.
82+
* Provides show-style and studio config, and TSR device actions.
83+
*
84+
* For playlists with multiple rundowns/show styles, only one show-style blueprint is invoked per snapshot.
85+
* Rundown selection order: current part, then next part, then first rundown by name (see job-worker
86+
* `pickRundownForPlaylistSnapshot`).
87+
*/
88+
export interface IPlaylistSnapshotCreatedContext extends IShowStyleContext, IExecuteTSRActionsContext {}

packages/corelib/src/worker/studio.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import {
1313
RundownId,
1414
RundownPlaylistId,
1515
SegmentId,
16+
SnapshotId,
1617
StudioId,
1718
} from '../dataModel/Ids.js'
1819
import { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob'
1920
import { CoreRundownPlaylistSnapshot } from '../snapshots.js'
20-
import { NoteSeverity } from '@sofie-automation/blueprints-integration'
21+
import { BlueprintSnapshotType, NoteSeverity } from '@sofie-automation/blueprints-integration'
2122
import { ITranslatableMessage } from '../TranslatableMessage.js'
2223
import { QuickLoopMarker } from '../dataModel/RundownPlaylist/RundownPlaylist.js'
2324
import { RundownTTimerIndex } from '../dataModel/RundownPlaylist/TTimers.js'
@@ -174,6 +175,11 @@ export enum StudioJobs {
174175
*/
175176
RestorePlaylistSnapshot = 'restorePlaylistSnapshot',
176177

178+
/**
179+
* Invoke {@link StudioBlueprintManifest.onSystemSnapshotCreated} for the studio after a system or debug snapshot is stored.
180+
*/
181+
OnSystemSnapshotCreated = 'onSystemSnapshotCreated',
182+
177183
/**
178184
* Run the Blueprint applyConfig for the studio
179185
*/
@@ -357,10 +363,30 @@ export type DebugRegenerateNextPartInstanceProps = RundownPlayoutPropsBase
357363
export type DebugSyncInfinitesForNextPartInstanceProps = RundownPlayoutPropsBase
358364

359365
export interface GeneratePlaylistSnapshotProps extends RundownPlayoutPropsBase {
360-
// Include all Instances, or just recent ones
366+
/** Include all part/piece instances, or only recent/non-reset instances. */
361367
full: boolean
362-
// Include the Timeline
368+
/** Include the timeline if the playlist is activated. */
363369
withTimeline: boolean
370+
/** Id of the snapshot (assigned in Meteor before the worker job is queued). Passed to the playlist snapshot blueprint hook. */
371+
snapshotId?: SnapshotId
372+
/** Human-readable reason for creating the snapshot. Passed to the playlist snapshot blueprint hook. */
373+
reason?: string
374+
}
375+
376+
/** Props for {@link StudioJobs.OnSystemSnapshotCreated}. */
377+
export interface OnSystemSnapshotCreatedProps {
378+
/** Id of the stored snapshot file. */
379+
snapshotId: SnapshotId
380+
/** Human-readable reason from the snapshot request. */
381+
reason: string
382+
type: BlueprintSnapshotType
383+
/** Snapshot options; `studioId` is the studio this worker job runs for. */
384+
options: {
385+
studioId?: StudioId
386+
withDeviceSnapshots?: boolean
387+
/** True when the stored snapshot is a full-system snapshot (not filtered to a single studio). */
388+
fullSystem?: boolean
389+
}
364390
}
365391
export interface GeneratePlaylistSnapshotResult {
366392
/**
@@ -499,6 +525,7 @@ export type StudioJobFunc = {
499525

500526
[StudioJobs.GeneratePlaylistSnapshot]: (data: GeneratePlaylistSnapshotProps) => GeneratePlaylistSnapshotResult
501527
[StudioJobs.RestorePlaylistSnapshot]: (data: RestorePlaylistSnapshotProps) => RestorePlaylistSnapshotResult
528+
[StudioJobs.OnSystemSnapshotCreated]: (data: OnSystemSnapshotCreatedProps) => void
502529
[StudioJobs.DebugCrash]: (data: DebugRegenerateNextPartInstanceProps) => void
503530

504531
[StudioJobs.BlueprintUpgradeForStudio]: () => void

packages/documentation/docs/for-developers/for-blueprint-developers/intro.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Documentation for this page is yet to be written.
1212

1313
Technically, a Blueprint is a JavaScript object, implementing one of the `BlueprintManifestBase` interfaces.
1414

15-
Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript.
15+
Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript.
1616

1717
:::info
1818
Note that the Runtime Environment for Blueprints in Sofie is plain JavaScript at [ES2015 level](https://en.wikipedia.org/wiki/ECMAScript_version_history#6th_edition_%E2%80%93_ECMAScript_2015), so other ways of building Blueprints are also possible.
@@ -28,10 +28,14 @@ Currently, there are three types of Blueprints:
2828

2929
These blueprints interpret the data coming from the [NRCS](../../user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md), meaning that they need to support the particular data structures that a given Ingest Gateway uses to store incoming data from the Rundown editor. They will need to convert Rundown Pages, Cues, Items, pieces of show script and other types of objects into [Sofie concepts](../../user-guide/concepts-and-architecture.md) such as Segments, Parts, Pieces and AdLibs.
3030

31+
Optional event hooks include playlist snapshot callbacks — see [Snapshot hooks](./snapshot-hooks.md).
32+
3133
# Studio Blueprints
3234

3335
These blueprints provide a "baseline" Timeline that is being used by your Studio whenever there isn't a Rundown active. They also handle combining Rundowns into RundownPlaylists. Via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.StudioBlueprintManifest.html#applyconfig) method, these Blueprints enable a _Configuration-as-Code_ approach to configuring connections to various elements of your Control Room and Studio.
3436

37+
Optional event hooks include system snapshot callbacks (with TSR device access) — see [Snapshot hooks](./snapshot-hooks.md).
38+
3539
# System Blueprints
3640

37-
These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints.
41+
These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints.

0 commit comments

Comments
 (0)