Skip to content

Commit 3f63362

Browse files
committed
Merge branch 'upstream/action-triggers-async' into release52
# Conflicts: # packages/webui/src/client/ui/Settings/components/triggeredActions/TriggeredActionEntry.tsx
2 parents 993a696 + 20ab08d commit 3f63362

File tree

21 files changed

+964
-682
lines changed

21 files changed

+964
-682
lines changed

meteor/server/api/deviceTriggers/StudioDeviceTriggerManager.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class StudioDeviceTriggerManager {
4343
StudioActionManagers.set(studioId, new StudioActionManager())
4444
}
4545

46-
updateTriggers(cache: ContentCache, showStyleBaseId: ShowStyleBaseId): void {
46+
async updateTriggers(cache: ContentCache, showStyleBaseId: ShowStyleBaseId): Promise<void> {
4747
const studioId = this.studioId
4848
this.#lastShowStyleBaseId = showStyleBaseId
4949

@@ -88,7 +88,7 @@ export class StudioDeviceTriggerManager {
8888

8989
const addedPreviewIds: PreviewWrappedAdLibId[] = []
9090

91-
Object.entries<SomeAction>(triggeredAction.actions).forEach(([key, action]) => {
91+
for (const [key, action] of Object.entries<SomeAction>(triggeredAction.actions)) {
9292
// Since the compiled action is cached using this actionId as a key, having the action
9393
// and the filterChain allows for a quicker invalidation without doing a deepEquals
9494
const actionId = protectString<DeviceActionId>(
@@ -106,9 +106,9 @@ export class StudioDeviceTriggerManager {
106106
}
107107
touchedActionIds.push(actionId)
108108

109-
Object.entries<SomeBlueprintTrigger>(triggeredAction.triggers).forEach(([key, trigger]) => {
109+
for (const [key, trigger] of Object.entries<SomeBlueprintTrigger>(triggeredAction.triggers)) {
110110
if (!isDeviceTrigger(trigger)) {
111-
return
111+
continue
112112
}
113113

114114
let deviceActionArguments: ShiftRegisterActionArguments | undefined = undefined
@@ -141,7 +141,7 @@ export class StudioDeviceTriggerManager {
141141
},
142142
})
143143
upsertedDeviceTriggerMountedActionIds.push(deviceTriggerMountedActionId)
144-
})
144+
}
145145

146146
if (!isPreviewableAction(thisAction)) {
147147
const adLibPreviewId = protectString(`${actionId}_preview`)
@@ -165,7 +165,7 @@ export class StudioDeviceTriggerManager {
165165

166166
addedPreviewIds.push(adLibPreviewId)
167167
} else {
168-
const previewedAdLibs = thisAction.preview(context)
168+
const previewedAdLibs = await thisAction.preview(context, null)
169169

170170
previewedAdLibs.forEach((adLib) => {
171171
const adLibPreviewId = protectString<PreviewWrappedAdLibId>(
@@ -195,7 +195,7 @@ export class StudioDeviceTriggerManager {
195195
addedPreviewIds.push(adLibPreviewId)
196196
})
197197
}
198-
})
198+
}
199199

200200
DeviceTriggerMountedActionAdlibsPreview.remove({
201201
triggeredActionId: triggeredAction._id,

meteor/server/api/deviceTriggers/observer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ MeteorStartupAsync(async () => {
4444
const manager = new StudioDeviceTriggerManager(studioId)
4545
const observer = new StudioObserver(studioId, (showStyleBaseId, cache) => {
4646
workInQueue(async () => {
47-
manager.updateTriggers(cache, showStyleBaseId)
47+
await manager.updateTriggers(cache, showStyleBaseId)
4848
})
4949

5050
return () => {
@@ -117,10 +117,12 @@ export async function receiveInputDeviceTrigger(
117117
if (!actionManager)
118118
throw new Meteor.Error(500, `No Studio Action Manager available to handle trigger in Studio "${studioId}"`)
119119

120-
DeviceTriggerMountedActions.find({
120+
const mountedActions = DeviceTriggerMountedActions.find({
121121
deviceId,
122122
deviceTriggerId: triggerId,
123-
}).forEach((mountedAction) => {
123+
}).fetch()
124+
125+
for (const mountedAction of mountedActions) {
124126
if (values && !_.isMatch(values, mountedAction.values)) return
125127
const executableAction = actionManager.getAction(mountedAction.actionId)
126128
if (!executableAction)
@@ -132,6 +134,6 @@ export async function receiveInputDeviceTrigger(
132134
const context = actionManager.getContext()
133135
if (!context) throw new Meteor.Error(500, `Undefined Device Trigger context for studio "${studioId}"`)
134136

135-
executableAction.execute((t: ITranslatableMessage) => t.key ?? t, `${deviceId}: ${triggerId}`, context)
136-
})
137+
await executableAction.execute((t: ITranslatableMessage) => t.key ?? t, `${deviceId}: ${triggerId}`, context)
138+
}
137139
}
Lines changed: 117 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,68 @@
1-
import { TriggersContext } from '@sofie-automation/meteor-lib/dist/triggers/triggersContext'
1+
import {
2+
TriggersAsyncCollection,
3+
TriggersContext,
4+
TriggerTrackerComputation,
5+
} from '@sofie-automation/meteor-lib/dist/triggers/triggersContext'
26
import { SINGLE_USE_TOKEN_SALT } from '@sofie-automation/meteor-lib/dist/api/userActions'
3-
import { assertNever, getHash, Time } from '../../lib/tempLib'
7+
import { assertNever, getHash, ProtectedString, Time } from '../../lib/tempLib'
48
import { getCurrentTime } from '../../lib/lib'
59
import { MeteorCall } from '../methods'
610
import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client'
711
import { UserAction } from '@sofie-automation/meteor-lib/dist/userAction'
812
import { TFunction } from 'i18next'
9-
import { Tracker } from 'meteor/tracker'
10-
1113
import { logger } from '../../logging'
1214
import { IBaseFilterLink, IRundownPlaylistFilterLink } from '@sofie-automation/blueprints-integration'
1315
import { PartId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
1416
import { DummyReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/reactive-var'
1517
import { ReactivePlaylistActionContext } from '@sofie-automation/meteor-lib/dist/triggers/actionFactory'
16-
import { MongoQuery } from '@sofie-automation/corelib/dist/mongo'
17-
import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections'
18-
import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction'
19-
import { AdLibPiece } from '@sofie-automation/corelib/dist/dataModel/AdLibPiece'
20-
import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances'
18+
import { FindOneOptions, FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo'
19+
import { DBRundownPlaylist, SelectedPartInstance } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
20+
import {
21+
AdLibActions,
22+
AdLibPieces,
23+
PartInstances,
24+
Parts,
25+
RundownBaselineAdLibActions,
26+
RundownBaselineAdLibPieces,
27+
RundownPlaylists,
28+
Rundowns,
29+
Segments,
30+
} from '../../collections'
2131
import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part'
22-
import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction'
23-
import { RundownBaselineAdLibItem } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibPiece'
24-
import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist'
25-
import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown'
26-
import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment'
27-
import { createSyncReadOnlyMongoCollection } from './triggersContextCollection'
32+
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
33+
import { AsyncOnlyReadOnlyMongoCollection } from '../../collections/collection'
2834

2935
export function hashSingleUseToken(token: string): string {
3036
return getHash(SINGLE_USE_TOKEN_SALT + token)
3137
}
3238

33-
/**
34-
* Some synchronous read-only collections to satisfy the TriggersContext interface
35-
*/
36-
const AdLibActions = createSyncReadOnlyMongoCollection<AdLibAction>(CollectionName.AdLibActions)
37-
const AdLibPieces = createSyncReadOnlyMongoCollection<AdLibPiece>(CollectionName.AdLibPieces)
38-
const PartInstances = createSyncReadOnlyMongoCollection<PartInstance>(CollectionName.PartInstances)
39-
const Parts = createSyncReadOnlyMongoCollection<DBPart>(CollectionName.Parts)
40-
const RundownBaselineAdLibActions = createSyncReadOnlyMongoCollection<RundownBaselineAdLibAction>(
41-
CollectionName.RundownBaselineAdLibActions
42-
)
43-
const RundownBaselineAdLibPieces = createSyncReadOnlyMongoCollection<RundownBaselineAdLibItem>(
44-
CollectionName.RundownBaselineAdLibPieces
45-
)
46-
const RundownPlaylists = createSyncReadOnlyMongoCollection<DBRundownPlaylist>(CollectionName.RundownPlaylists)
47-
const Rundowns = createSyncReadOnlyMongoCollection<DBRundown>(CollectionName.Rundowns)
48-
const Segments = createSyncReadOnlyMongoCollection<DBSegment>(CollectionName.Segments)
39+
class MeteorTriggersCollectionWrapper<DBInterface extends { _id: ProtectedString<any> }>
40+
implements TriggersAsyncCollection<DBInterface>
41+
{
42+
readonly #collection: AsyncOnlyReadOnlyMongoCollection<DBInterface>
43+
44+
constructor(collection: AsyncOnlyReadOnlyMongoCollection<DBInterface>) {
45+
this.#collection = collection
46+
}
47+
48+
async findFetchAsync(
49+
_computation: TriggerTrackerComputation | null,
50+
selector: MongoQuery<DBInterface>,
51+
options?: FindOptions<DBInterface>
52+
): Promise<Array<DBInterface>> {
53+
// Note: the _computation is not used, since we are not using Tracker server-side
54+
return this.#collection.findFetchAsync(selector, options)
55+
}
56+
57+
async findOneAsync(
58+
_computation: TriggerTrackerComputation | null,
59+
selector: MongoQuery<DBInterface> | DBInterface['_id'],
60+
options?: FindOneOptions<DBInterface>
61+
): Promise<DBInterface | undefined> {
62+
// Note: the _computation is not used, since we are not using Tracker server-side
63+
return this.#collection.findOneAsync(selector, options)
64+
}
65+
}
4966

5067
export const MeteorTriggersContext: TriggersContext = {
5168
MeteorCall,
@@ -54,14 +71,14 @@ export const MeteorTriggersContext: TriggersContext = {
5471

5572
isClient: false,
5673

57-
AdLibActions,
58-
AdLibPieces,
59-
Parts,
60-
RundownBaselineAdLibActions,
61-
RundownBaselineAdLibPieces,
62-
RundownPlaylists,
63-
Rundowns,
64-
Segments,
74+
AdLibActions: new MeteorTriggersCollectionWrapper(AdLibActions),
75+
AdLibPieces: new MeteorTriggersCollectionWrapper(AdLibPieces),
76+
Parts: new MeteorTriggersCollectionWrapper(Parts),
77+
RundownBaselineAdLibActions: new MeteorTriggersCollectionWrapper(RundownBaselineAdLibActions),
78+
RundownBaselineAdLibPieces: new MeteorTriggersCollectionWrapper(RundownBaselineAdLibPieces),
79+
RundownPlaylists: new MeteorTriggersCollectionWrapper(RundownPlaylists),
80+
Rundowns: new MeteorTriggersCollectionWrapper(Rundowns),
81+
Segments: new MeteorTriggersCollectionWrapper(Segments),
6582

6683
hashSingleUseToken,
6784

@@ -81,73 +98,92 @@ export const MeteorTriggersContext: TriggersContext = {
8198
)
8299
},
83100

84-
nonreactiveTracker: Tracker.nonreactive,
101+
withComputation: async (_computation, func) => {
102+
// Note: the _computation is not used, since we are not using Tracker server-side
103+
return func()
104+
},
85105

86-
memoizedIsolatedAutorun: <T extends (...args: any) => any>(
87-
fnc: T,
106+
memoizedIsolatedAutorun: async <TArgs extends any[], TRes>(
107+
computation: TriggerTrackerComputation | null,
108+
fnc: (computation: TriggerTrackerComputation | null, ...args: TArgs) => Promise<TRes>,
88109
_functionName: string,
89-
...params: Parameters<T>
90-
): ReturnType<T> => {
91-
return fnc(...(params as any))
110+
...params: TArgs
111+
): Promise<TRes> => {
112+
return fnc(computation, ...params)
92113
},
93114

94115
createContextForRundownPlaylistChain,
95116
}
96117

97-
function createContextForRundownPlaylistChain(
118+
async function createContextForRundownPlaylistChain(
98119
studioId: StudioId,
99120
filterChain: IBaseFilterLink[]
100-
): ReactivePlaylistActionContext | undefined {
101-
const playlist = rundownPlaylistFilter(
121+
): Promise<ReactivePlaylistActionContext | undefined> {
122+
const playlist = await rundownPlaylistFilter(
102123
studioId,
103124
filterChain.filter((link) => link.object === 'rundownPlaylist') as IRundownPlaylistFilterLink[]
104125
)
105126

106127
if (!playlist) return undefined
107128

108-
let currentPartId: PartId | null = null,
109-
nextPartId: PartId | null = null,
110-
currentPartInstance: PartInstance | null = null,
111-
currentSegmentPartIds: PartId[] = [],
112-
nextSegmentPartIds: PartId[] = []
113-
114-
if (playlist.currentPartInfo) {
115-
currentPartInstance = PartInstances.findOne(playlist.currentPartInfo.partInstanceId) ?? null
116-
const currentPart = currentPartInstance?.part ?? null
117-
if (currentPart) {
118-
currentPartId = currentPart._id
119-
currentSegmentPartIds = Parts.find({
120-
segmentId: currentPart.segmentId,
121-
}).map((part) => part._id)
122-
}
123-
}
124-
if (playlist.nextPartInfo) {
125-
const nextPart = PartInstances.findOne(playlist.nextPartInfo.partInstanceId)?.part ?? null
126-
if (nextPart) {
127-
nextPartId = nextPart._id
128-
nextSegmentPartIds = Parts.find({
129-
segmentId: nextPart.segmentId,
130-
}).map((part) => part._id)
131-
}
132-
}
129+
const [currentPartInfo, nextPartInfo] = await Promise.all([
130+
fetchInfoForSelectedPart(playlist.currentPartInfo),
131+
fetchInfoForSelectedPart(playlist.nextPartInfo),
132+
])
133133

134134
return {
135135
studioId: new DummyReactiveVar(studioId),
136136
rundownPlaylistId: new DummyReactiveVar(playlist?._id),
137137
rundownPlaylist: new DummyReactiveVar(playlist),
138-
currentRundownId: new DummyReactiveVar(currentPartInstance?.rundownId ?? playlist.rundownIdsInOrder[0] ?? null),
139-
currentPartId: new DummyReactiveVar(currentPartId),
140-
currentSegmentPartIds: new DummyReactiveVar(currentSegmentPartIds),
141-
nextPartId: new DummyReactiveVar(nextPartId),
142-
nextSegmentPartIds: new DummyReactiveVar(nextSegmentPartIds),
138+
currentRundownId: new DummyReactiveVar(
139+
playlist.currentPartInfo?.rundownId ?? playlist.rundownIdsInOrder[0] ?? null
140+
),
141+
currentPartId: new DummyReactiveVar(currentPartInfo?.partId ?? null),
142+
currentSegmentPartIds: new DummyReactiveVar(currentPartInfo?.segmentPartIds ?? []),
143+
nextPartId: new DummyReactiveVar(nextPartInfo?.partId ?? null),
144+
nextSegmentPartIds: new DummyReactiveVar(nextPartInfo?.segmentPartIds ?? []),
143145
currentPartInstanceId: new DummyReactiveVar(playlist.currentPartInfo?.partInstanceId ?? null),
144146
}
145147
}
146148

147-
function rundownPlaylistFilter(
149+
async function fetchInfoForSelectedPart(partInfo: SelectedPartInstance | null): Promise<{
150+
partId: PartId
151+
segmentPartIds: PartId[]
152+
} | null> {
153+
if (!partInfo) return null
154+
155+
const partInstance = (await PartInstances.findOneAsync(partInfo.partInstanceId, {
156+
projection: {
157+
// @ts-expect-error deep property
158+
'part._id': 1,
159+
segmentId: 1,
160+
},
161+
})) as (Pick<DBPartInstance, 'segmentId'> & { part: Pick<DBPart, '_id'> }) | null
162+
163+
if (!partInstance) return null
164+
165+
const partId = partInstance.part._id
166+
const segmentPartIds = await Parts.findFetchAsync(
167+
{
168+
segmentId: partInstance.segmentId,
169+
},
170+
{
171+
projection: {
172+
_id: 1,
173+
},
174+
}
175+
).then((parts) => parts.map((part) => part._id))
176+
177+
return {
178+
partId,
179+
segmentPartIds,
180+
}
181+
}
182+
183+
async function rundownPlaylistFilter(
148184
studioId: StudioId,
149185
filterChain: IRundownPlaylistFilterLink[]
150-
): DBRundownPlaylist | undefined {
186+
): Promise<DBRundownPlaylist | undefined> {
151187
const selector: MongoQuery<DBRundownPlaylist> = {
152188
$and: [
153189
{
@@ -181,5 +217,5 @@ function rundownPlaylistFilter(
181217
}
182218
})
183219

184-
return RundownPlaylists.findOne(selector)
220+
return RundownPlaylists.findOneAsync(selector)
185221
}

0 commit comments

Comments
 (0)