Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: customisable package status messages #47

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions meteor/server/api/blueprints/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ async function innerUploadBlueprint(
newBlueprint.showStyleConfigSchema = blueprintManifest.showStyleConfigSchema
newBlueprint.showStyleConfigPresets = blueprintManifest.configPresets
newBlueprint.hasFixUpFunction = !!blueprintManifest.fixUpConfig
newBlueprint.packageStatusMessages = blueprintManifest.packageStatusMessages
} else if (blueprintManifest.blueprintType === BlueprintManifestType.STUDIO) {
newBlueprint.studioConfigSchema = blueprintManifest.studioConfigSchema
newBlueprint.studioConfigPresets = blueprintManifest.configPresets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { defaultStudio } from '../../../../__mocks__/defaultCollectionObjects'
import { MediaObjects } from '../../../collections'
import { PieceDependencies } from '../common'
import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants'
import { PieceContentStatusMessageFactory } from '../messageFactory'

const mockMediaObjectsCollection = MongoMock.getInnerMockCollection<MediaObject>(MediaObjects)

Expand Down Expand Up @@ -449,7 +450,9 @@ describe('lib/mediaObjects', () => {
timelineObjectsString: EmptyPieceTimelineObjectsBlob,
})

const status1 = await checkPieceContentStatusAndDependencies(mockStudio, piece1, sourcelayer1)
const messageFactory = new PieceContentStatusMessageFactory(undefined)

const status1 = await checkPieceContentStatusAndDependencies(mockStudio, messageFactory, piece1, sourcelayer1)
expect(status1[0].status).toEqual(PieceStatusCode.OK)
expect(status1[0].messages).toHaveLength(0)
expect(status1[1]).toMatchObject(
Expand All @@ -460,7 +463,7 @@ describe('lib/mediaObjects', () => {
})
)

const status2 = await checkPieceContentStatusAndDependencies(mockStudio, piece2, sourcelayer1)
const status2 = await checkPieceContentStatusAndDependencies(mockStudio, messageFactory, piece2, sourcelayer1)
expect(status2[0].status).toEqual(PieceStatusCode.SOURCE_BROKEN)
expect(status2[0].messages).toHaveLength(1)
expect(status2[0].messages[0]).toMatchObject({
Expand All @@ -474,7 +477,7 @@ describe('lib/mediaObjects', () => {
})
)

const status3 = await checkPieceContentStatusAndDependencies(mockStudio, piece3, sourcelayer1)
const status3 = await checkPieceContentStatusAndDependencies(mockStudio, messageFactory, piece3, sourcelayer1)
expect(status3[0].status).toEqual(PieceStatusCode.SOURCE_MISSING)
expect(status3[0].messages).toHaveLength(1)
expect(status3[0].messages[0]).toMatchObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo
import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction'
import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece'
import { BlueprintId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint'

export interface SourceLayersDoc {
_id: ShowStyleBaseId
Expand Down Expand Up @@ -53,10 +54,17 @@ export const showStyleBaseFieldSpecifier = literal<
sourceLayersWithOverrides: 1,
})

export type BlueprintFields = '_id' | 'packageStatusMessages'
export const blueprintFieldSpecifier = literal<MongoFieldSpecifierOnesStrict<Pick<Blueprint, BlueprintFields>>>({
_id: 1,
packageStatusMessages: 1,
})

export interface BucketContentCache {
BucketAdLibs: ReactiveCacheCollection<Pick<BucketAdLib, BucketAdLibFields>>
BucketAdLibActions: ReactiveCacheCollection<Pick<BucketAdLibAction, BucketActionFields>>
ShowStyleSourceLayers: ReactiveCacheCollection<SourceLayersDoc>
Blueprints: ReactiveCacheCollection<Pick<Blueprint, BlueprintFields>>
}

export function createReactiveContentCache(): BucketContentCache {
Expand All @@ -66,6 +74,7 @@ export function createReactiveContentCache(): BucketContentCache {
'bucketAdlibActions'
),
ShowStyleSourceLayers: new ReactiveCacheCollection<SourceLayersDoc>('sourceLayers'),
Blueprints: new ReactiveCacheCollection<Pick<Blueprint, BlueprintFields>>('blueprints'),
}

return cache
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Meteor } from 'meteor/meteor'
import { BucketId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { BlueprintId, BucketId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { logger } from '../../../logging'
import {
blueprintFieldSpecifier,
bucketActionFieldSpecifier,
bucketAdlibFieldSpecifier,
BucketContentCache,
ShowStyleBaseFields,
showStyleBaseFieldSpecifier,
SourceLayersDoc,
} from './bucketContentCache'
import { BucketAdLibActions, BucketAdLibs, ShowStyleBases } from '../../../collections'
import { Blueprints, BucketAdLibActions, BucketAdLibs, ShowStyleBases } from '../../../collections'
import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase'
import { equivalentArrays } from '@sofie-automation/shared-lib/dist/lib/lib'
import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
Expand All @@ -33,6 +34,9 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle {
#showStyleBaseIds: ShowStyleBaseId[] = []
#showStyleBaseIdObserver!: ReactiveMongoObserverGroupHandle

#blueprintIds: BlueprintId[] = []
#blueprintIdObserver!: ReactiveMongoObserverGroupHandle

#disposed = false

private constructor(cache: BucketContentCache) {
Expand All @@ -59,13 +63,16 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle {
added: (doc) => {
const newDoc = convertShowStyleBase(doc)
cache.ShowStyleSourceLayers.upsert(doc._id, { $set: newDoc as Partial<Document> })
observer.updateBlueprintIds()
},
changed: (doc) => {
const newDoc = convertShowStyleBase(doc)
cache.ShowStyleSourceLayers.upsert(doc._id, { $set: newDoc as Partial<Document> })
observer.updateBlueprintIds()
},
removed: (doc) => {
cache.ShowStyleSourceLayers.remove(doc._id)
observer.updateBlueprintIds()
},
},
{
Expand All @@ -75,6 +82,27 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle {
]
})

// Run the Blueprint query in a ReactiveMongoObserverGroup, so that it can be restarted whenever
observer.#blueprintIdObserver = await ReactiveMongoObserverGroup(async () => {
// Clear already cached data
cache.Blueprints.remove({})

logger.silly(`optimized observer restarting ${observer.#blueprintIds}`)

return [
Blueprints.observeChanges(
{
// We can use the `this.#blueprintIds` here, as this is restarted every time that property changes
_id: { $in: observer.#blueprintIds },
},
cache.Blueprints.link(),
{
projection: blueprintFieldSpecifier,
}
),
]
})

// Subscribe to the database, and pipe any updates into the ReactiveCacheCollections
// This takes ownership of the #showStyleBaseIdObserver, and will stop it if this throws
observer.#observers = await waitForAllObserversReady([
Expand Down Expand Up @@ -106,6 +134,7 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle {
),

observer.#showStyleBaseIdObserver,
observer.#blueprintIdObserver,
])

return observer
Expand All @@ -132,6 +161,22 @@ export class BucketContentObserver implements Meteor.LiveQueryHandle {
REACTIVITY_DEBOUNCE
)

private updateBlueprintIds = _.debounce(
Meteor.bindEnvironment(() => {
if (this.#disposed) return

const newBlueprintIds = _.uniq(this.#cache.ShowStyleSourceLayers.find({}).map((rd) => rd.blueprintId))

if (!equivalentArrays(newBlueprintIds, this.#blueprintIds)) {
logger.silly(`optimized observer changed ids ${JSON.stringify(newBlueprintIds)} ${this.#blueprintIds}`)
this.#blueprintIds = newBlueprintIds
// trigger the rundown group to restart
this.#blueprintIdObserver.restart()
}
}),
REACTIVITY_DEBOUNCE
)

public get cache(): BucketContentCache {
return this.#cache
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BucketId,
ExpectedPackageId,
PackageContainerPackageId,
ShowStyleBaseId,
StudioId,
} from '@sofie-automation/corelib/dist/dataModel/Ids'
import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mongo'
Expand Down Expand Up @@ -37,6 +38,7 @@ import { regenerateForBucketActionIds, regenerateForBucketAdLibIds } from './reg
import { PieceContentStatusStudio } from '../checkPieceContentStatus'
import { check } from 'meteor/check'
import { triggerWriteAccessBecauseNoCheckNecessary } from '../../../security/securityVerify'
import { PieceContentStatusMessageFactory } from '../messageFactory'

interface UIBucketContentStatusesArgs {
readonly studioId: StudioId
Expand All @@ -48,6 +50,8 @@ interface UIBucketContentStatusesState {

studio: PieceContentStatusStudio

showStyleMessageFactories: Map<ShowStyleBaseId, PieceContentStatusMessageFactory>

adlibDependencies: Map<BucketAdLibId, PieceDependencies>
actionDependencies: Map<BucketAdLibActionId, PieceDependencies>
}
Expand Down Expand Up @@ -111,6 +115,16 @@ async function setupUIBucketContentStatusesPublicationObservers(
changed: (id) => triggerUpdate(trackActionChange(protectString(id))),
removed: (id) => triggerUpdate(trackActionChange(protectString(id))),
}),
contentCache.Blueprints.find({}).observeChanges({
added: () => triggerUpdate({ invalidateAll: true }),
changed: () => triggerUpdate({ invalidateAll: true }),
removed: () => triggerUpdate({ invalidateAll: true }),
}),
contentCache.ShowStyleSourceLayers.find({}).observeChanges({
added: () => triggerUpdate({ invalidateAll: true }),
changed: () => triggerUpdate({ invalidateAll: true }),
removed: () => triggerUpdate({ invalidateAll: true }),
}),

Studios.observeChanges(
{ _id: bucket.studioId },
Expand Down Expand Up @@ -198,14 +212,26 @@ async function manipulateUIBucketContentStatusesPublicationData(

let regenerateActionIds: Set<BucketAdLibActionId>
let regenerateAdlibIds: Set<BucketAdLibId>
if (!state.adlibDependencies || !state.actionDependencies || invalidateAllItems) {
if (
!state.adlibDependencies ||
!state.actionDependencies ||
!state.showStyleMessageFactories ||
invalidateAllItems
) {
state.adlibDependencies = new Map()
state.actionDependencies = new Map()
state.showStyleMessageFactories = new Map()

// force every piece to be regenerated
collection.remove(null)
regenerateAdlibIds = new Set(state.contentCache.BucketAdLibs.find({}).map((p) => p._id))
regenerateActionIds = new Set(state.contentCache.BucketAdLibActions.find({}).map((p) => p._id))

// prepare the message factories
for (const showStyle of state.contentCache.ShowStyleSourceLayers.find({})) {
const blueprint = state.contentCache.Blueprints.findOne(showStyle.blueprintId)
state.showStyleMessageFactories.set(showStyle._id, new PieceContentStatusMessageFactory(blueprint))
}
} else {
regenerateAdlibIds = new Set(updateProps.invalidateBucketAdlibIds)
regenerateActionIds = new Set(updateProps.invalidateBucketActionIds)
Expand All @@ -227,13 +253,15 @@ async function manipulateUIBucketContentStatusesPublicationData(
state.contentCache,
state.studio,
state.adlibDependencies,
state.showStyleMessageFactories,
collection,
regenerateAdlibIds
)
await regenerateForBucketActionIds(
state.contentCache,
state.studio,
state.actionDependencies,
state.showStyleMessageFactories,
collection,
regenerateActionIds
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BucketAdLibActionId, BucketAdLibId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { BucketAdLibActionId, BucketAdLibId, ShowStyleBaseId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { ReadonlyDeep } from 'type-fest'
import { UIBucketContentStatus } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications'
import { literal, protectString } from '../../../lib/tempLib'
Expand All @@ -11,6 +11,7 @@ import {
PieceContentStatusPiece,
PieceContentStatusStudio,
} from '../checkPieceContentStatus'
import type { PieceContentStatusMessageFactory } from '../messageFactory'

/**
* Regenerating the status for the provided AdLibActionId
Expand All @@ -20,6 +21,7 @@ export async function regenerateForBucketAdLibIds(
contentCache: ReadonlyDeep<BucketContentCache>,
uiStudio: PieceContentStatusStudio,
dependenciesState: Map<BucketAdLibId, PieceDependencies>,
messageFactories: Map<ShowStyleBaseId, PieceContentStatusMessageFactory>,
collection: CustomPublishCollection<UIBucketContentStatus>,
regenerateIds: Set<BucketAdLibId>
): Promise<void> {
Expand All @@ -45,6 +47,7 @@ export async function regenerateForBucketAdLibIds(
if (sourceLayer) {
const [status, itemDependencies] = await checkPieceContentStatusAndDependencies(
uiStudio,
messageFactories.get(actionDoc.showStyleBaseId),
actionDoc,
sourceLayer
)
Expand Down Expand Up @@ -79,6 +82,7 @@ export async function regenerateForBucketActionIds(
contentCache: ReadonlyDeep<BucketContentCache>,
uiStudio: PieceContentStatusStudio,
dependenciesState: Map<BucketAdLibActionId, PieceDependencies>,
messageFactories: Map<ShowStyleBaseId, PieceContentStatusMessageFactory>,
collection: CustomPublishCollection<UIBucketContentStatus>,
regenerateIds: Set<BucketAdLibActionId>
): Promise<void> {
Expand Down Expand Up @@ -106,11 +110,16 @@ export async function regenerateForBucketActionIds(
const fakedPiece = literal<PieceContentStatusPiece>({
_id: protectString(`${actionDoc._id}`),
content: 'content' in actionDoc.display ? actionDoc.display.content : {},
name:
typeof actionDoc.display.label === 'string'
? actionDoc.display.label
: actionDoc.display.label.key,
expectedPackages: actionDoc.expectedPackages,
})

const [status, itemDependencies] = await checkPieceContentStatusAndDependencies(
uiStudio,
messageFactories.get(actionDoc.showStyleBaseId),
fakedPiece,
sourceLayer
)
Expand Down
Loading
Loading