Skip to content

Commit c2a6522

Browse files
committed
feat: convert action-triggers computation to async
1 parent 91e6a40 commit c2a6522

File tree

21 files changed

+962
-683
lines changed

21 files changed

+962
-683
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: 114 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,66 @@
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+
return this.#collection.findFetchAsync(selector, options)
54+
}
55+
56+
async findOneAsync(
57+
_computation: TriggerTrackerComputation | null,
58+
selector: MongoQuery<DBInterface> | DBInterface['_id'],
59+
options?: FindOneOptions<DBInterface>
60+
): Promise<DBInterface | undefined> {
61+
return this.#collection.findOneAsync(selector, options)
62+
}
63+
}
4964

5065
export const MeteorTriggersContext: TriggersContext = {
5166
MeteorCall,
@@ -54,14 +69,14 @@ export const MeteorTriggersContext: TriggersContext = {
5469

5570
isClient: false,
5671

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

6681
hashSingleUseToken,
6782

@@ -81,73 +96,91 @@ export const MeteorTriggersContext: TriggersContext = {
8196
)
8297
},
8398

84-
nonreactiveTracker: Tracker.nonreactive,
99+
withComputation: async (_computation, func) => {
100+
return func()
101+
},
85102

86-
memoizedIsolatedAutorun: <T extends (...args: any) => any>(
87-
fnc: T,
103+
memoizedIsolatedAutorun: async <TArgs extends any[], TRes>(
104+
computation: TriggerTrackerComputation | null,
105+
fnc: (computation: TriggerTrackerComputation | null, ...args: TArgs) => Promise<TRes>,
88106
_functionName: string,
89-
...params: Parameters<T>
90-
): ReturnType<T> => {
91-
return fnc(...(params as any))
107+
...params: TArgs
108+
): Promise<TRes> => {
109+
return fnc(computation, ...params)
92110
},
93111

94112
createContextForRundownPlaylistChain,
95113
}
96114

97-
function createContextForRundownPlaylistChain(
115+
async function createContextForRundownPlaylistChain(
98116
studioId: StudioId,
99117
filterChain: IBaseFilterLink[]
100-
): ReactivePlaylistActionContext | undefined {
101-
const playlist = rundownPlaylistFilter(
118+
): Promise<ReactivePlaylistActionContext | undefined> {
119+
const playlist = await rundownPlaylistFilter(
102120
studioId,
103121
filterChain.filter((link) => link.object === 'rundownPlaylist') as IRundownPlaylistFilterLink[]
104122
)
105123

106124
if (!playlist) return undefined
107125

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-
}
126+
const [currentPartInfo, nextPartInfo] = await Promise.all([
127+
fetchInfoForSelectedPart(playlist.currentPartInfo),
128+
fetchInfoForSelectedPart(playlist.nextPartInfo),
129+
])
133130

134131
return {
135132
studioId: new DummyReactiveVar(studioId),
136133
rundownPlaylistId: new DummyReactiveVar(playlist?._id),
137134
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),
135+
currentRundownId: new DummyReactiveVar(
136+
playlist.currentPartInfo?.rundownId ?? playlist.rundownIdsInOrder[0] ?? null
137+
),
138+
currentPartId: new DummyReactiveVar(currentPartInfo?.partId ?? null),
139+
currentSegmentPartIds: new DummyReactiveVar(currentPartInfo?.segmentPartIds ?? []),
140+
nextPartId: new DummyReactiveVar(nextPartInfo?.partId ?? null),
141+
nextSegmentPartIds: new DummyReactiveVar(nextPartInfo?.segmentPartIds ?? []),
143142
currentPartInstanceId: new DummyReactiveVar(playlist.currentPartInfo?.partInstanceId ?? null),
144143
}
145144
}
146145

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

184-
return RundownPlaylists.findOne(selector)
217+
return RundownPlaylists.findOneAsync(selector)
185218
}

0 commit comments

Comments
 (0)