Skip to content

Commit cbed799

Browse files
authored
Merge pull request #1367 from tv2norge-collab/contribute/EAV-450
refactor(LSG): get rid of async in handlers; extract shared logic to base classes
2 parents 955d96e + 4d991aa commit cbed799

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1348
-1627
lines changed

meteor/server/api/rest/v0/__tests__/rest.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { MeteorMethodSignatures } from '../../../../methods'
55
import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client'
66
import { callKoaRoute } from '../../../../../__mocks__/koa-util'
77
import { createLegacyApiRouter } from '..'
8-
import '../../../userActions.ts' // required to get the UserActionsAPI methods populated
8+
import '../../../userActions' // required to get the UserActionsAPI methods populated
99

1010
// we don't want the deviceTriggers observer to start up at this time
1111
jest.mock('../../../deviceTriggers/observer')
1212

13-
import '../index.ts'
13+
import '../index'
1414

1515
describe('REST API', () => {
1616
describe('UNSTABLE v0', () => {

meteor/server/migration/1_52_0.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CoreSystem, PeripheralDevices, Studios, TriggeredActions } from '../col
33
import {
44
convertObjectIntoOverrides,
55
ObjectOverrideSetOp,
6+
ObjectWithOverrides,
67
wrapDefaultObject,
78
} from '@sofie-automation/corelib/dist/settings/objectWithOverrides'
89
import {
@@ -16,7 +17,7 @@ import { DEFAULT_CORE_TRIGGER_IDS } from './upgrades/defaultSystemActionTriggers
1617
import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem'
1718
import { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings'
1819
import { logger } from '../logging'
19-
import { literal, unprotectString } from '../lib/tempLib'
20+
import { assertNever, literal, unprotectString } from '../lib/tempLib'
2021

2122
// Release 52
2223

@@ -71,15 +72,27 @@ export const addSteps = addMigrationSteps('1.52.0', [
7172
const studios = await Studios.findFetchAsync({ routeSetsWithOverrides: { $exists: true } })
7273

7374
for (const studio of studios) {
74-
const routeSetsDefaults = studio.routeSetsWithOverrides.defaults as any as Record<
75-
string,
76-
StudioRouteSet
77-
>
75+
// .abPlayers in the defaults:
76+
const routeSetsDefaults = studio.routeSetsWithOverrides.defaults
7877
for (const key of Object.keys(routeSetsDefaults)) {
7978
if (!routeSetsDefaults[key].abPlayers) {
8079
return 'AB players must be added to routeSetsWithOverrides'
8180
}
8281
}
82+
// .abPlayers in the overrides:
83+
for (const override of studio.routeSetsWithOverrides.overrides) {
84+
if (override.op === 'set') {
85+
const value = override.value as StudioRouteSet
86+
87+
if (!value.abPlayers) {
88+
return 'AB players must be added to routeSetsWithOverrides'
89+
}
90+
} else if (override.op === 'delete') {
91+
// ignore this
92+
} else {
93+
assertNever(override)
94+
}
95+
}
8396
}
8497

8598
return false
@@ -88,16 +101,29 @@ export const addSteps = addMigrationSteps('1.52.0', [
88101
const studios = await Studios.findFetchAsync({ routeSetsWithOverrides: { $exists: true } })
89102

90103
for (const studio of studios) {
91-
const newRouteSetswithOverrides = studio.routeSetsWithOverrides
92-
for (const key of Object.keys(newRouteSetswithOverrides.defaults)) {
93-
if (!newRouteSetswithOverrides.defaults[key].abPlayers) {
94-
newRouteSetswithOverrides.defaults[key].abPlayers = []
104+
const newRouteSetsWithOverrides = studio.routeSetsWithOverrides
105+
106+
// .abPlayers in the defaults:
107+
const routeSetsDefaults = newRouteSetsWithOverrides.defaults
108+
for (const key of Object.keys(routeSetsDefaults)) {
109+
if (!routeSetsDefaults[key].abPlayers) {
110+
routeSetsDefaults[key].abPlayers = []
111+
}
112+
}
113+
// .abPlayers in the overrides:
114+
for (const override of newRouteSetsWithOverrides.overrides) {
115+
if (override.op === 'set') {
116+
const value = override.value as StudioRouteSet
117+
118+
if (!value.abPlayers) {
119+
value.abPlayers = []
120+
}
95121
}
96122
}
97123

98124
await Studios.updateAsync(studio._id, {
99125
$set: {
100-
routeSetsWithOverrides: newRouteSetswithOverrides,
126+
routeSetsWithOverrides: newRouteSetsWithOverrides,
101127
},
102128
})
103129
}
@@ -243,7 +269,9 @@ export const addSteps = addMigrationSteps('1.52.0', [
243269
//@ts-expect-error settings is typed as Record<string, StudioRouteSet>
244270
const oldSettings = studio.settings
245271

246-
const newSettings = wrapDefaultObject<IStudioSettings>(oldSettings || {})
272+
const newSettings = convertObjectIntoOverrides(
273+
oldSettings || {}
274+
) as unknown as ObjectWithOverrides<IStudioSettings>
247275

248276
await Studios.updateAsync(studio._id, {
249277
$set: {
@@ -279,7 +307,7 @@ export const addSteps = addMigrationSteps('1.52.0', [
279307
for (const system of systems) {
280308
const oldSystem = system as ICoreSystem as PartialOldICoreSystem
281309

282-
const newSettings = wrapDefaultObject<ICoreSystemSettings>({
310+
const newSettings = convertObjectIntoOverrides({
283311
cron: {
284312
casparCGRestart: {
285313
enabled: false,
@@ -291,7 +319,7 @@ export const addSteps = addMigrationSteps('1.52.0', [
291319
},
292320
support: oldSystem.support ?? { message: '' },
293321
evaluationsMessage: oldSystem.evaluations ?? { enabled: false, heading: '', message: '' },
294-
})
322+
}) as unknown as ObjectWithOverrides<ICoreSystemSettings>
295323

296324
await CoreSystem.updateAsync(system._id, {
297325
$set: {

packages/corelib/src/worker/studio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export enum StudioJobs {
185185
BlueprintIgnoreFixUpConfigForStudio = 'blueprintIgnoreFixUpConfigForStudio',
186186

187187
/**
188-
* Activate AdlibTesting (Rehearsal Mode) mode for the Rundown containing the nexted Part.
188+
* Activate AdlibTesting for the Rundown containing the nexted part.
189189
*/
190190
ActivateAdlibTesting = 'activateAdlibTesting',
191191

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { CorelibPubSubCollections, CorelibPubSubTypes } from '@sofie-automation/corelib/dist/pubsub'
2+
import {
3+
StudioId,
4+
CoreConnection,
5+
ProtectedString,
6+
Collection as CoreCollection,
7+
CollectionDocCheck,
8+
} from '@sofie-automation/server-core-integration'
9+
import throttleToNextTick from '@sofie-automation/shared-lib/dist/lib/throttleToNextTick'
10+
import * as _ from 'underscore'
11+
import { Logger } from 'winston'
12+
import { CoreHandler } from './coreHandler'
13+
import { arePropertiesShallowEqual } from './helpers/equality'
14+
import { CollectionHandlers } from './liveStatusServer'
15+
16+
export type ObserverCallback<T, K extends keyof T> = (data: Pick<T, K> | undefined) => void
17+
18+
export const DEFAULT_THROTTLE_PERIOD_MS = 20
19+
20+
export abstract class CollectionBase<T, TCollection extends keyof CorelibPubSubCollections> {
21+
protected _name: string
22+
protected _collectionName: TCollection
23+
protected _logger: Logger
24+
protected _coreHandler: CoreHandler
25+
protected _studioId!: StudioId
26+
protected _observers: Map<
27+
ObserverCallback<T, keyof T>,
28+
{ keysToPick: readonly (keyof T)[] | undefined; lastData: T | undefined }
29+
> = new Map()
30+
protected _collectionData: T | undefined
31+
32+
protected get _core(): CoreConnection<CorelibPubSubTypes, CorelibPubSubCollections> {
33+
return this._coreHandler.core
34+
}
35+
protected throttledChanged: () => void
36+
37+
constructor(
38+
collection: TCollection,
39+
logger: Logger,
40+
coreHandler: CoreHandler,
41+
throttlePeriodMs = DEFAULT_THROTTLE_PERIOD_MS
42+
) {
43+
this._name = this.constructor.name
44+
this._collectionName = collection
45+
this._logger = logger
46+
this._coreHandler = coreHandler
47+
48+
this.throttledChanged = throttleToNextTick(
49+
throttlePeriodMs > 0
50+
? _.throttle(() => this.changed(), throttlePeriodMs, { leading: true, trailing: true })
51+
: () => this.changed()
52+
)
53+
54+
this._logger.info(`Starting ${this._name} handler`)
55+
}
56+
57+
init(_handlers: CollectionHandlers): void {
58+
if (!this._coreHandler.studioId) throw new Error('StudioId is not defined')
59+
this._studioId = this._coreHandler.studioId
60+
}
61+
62+
close(): void {
63+
this._logger.info(`Closing ${this._name} handler`)
64+
}
65+
66+
subscribe<K extends keyof T>(callback: ObserverCallback<T, K>, keysToPick?: readonly K[]): void {
67+
//this._logger.info(`${name}' added observer for '${this._name}'`)
68+
if (this._collectionData) callback(this._collectionData)
69+
this._observers.set(callback, { keysToPick, lastData: this.shallowClone(this._collectionData) })
70+
}
71+
72+
/**
73+
* Called after a batch of updates to documents in the collection
74+
*/
75+
protected changed(): void {
76+
// override me
77+
}
78+
79+
notify(data: T | undefined): void {
80+
for (const [observer, o] of this._observers) {
81+
if (
82+
!o.lastData ||
83+
!o.keysToPick ||
84+
!data ||
85+
!arePropertiesShallowEqual(o.lastData, data, undefined, o.keysToPick)
86+
) {
87+
observer(data)
88+
o.lastData = this.shallowClone(data)
89+
}
90+
}
91+
}
92+
93+
protected shallowClone(data: T | undefined): T | undefined {
94+
if (data === undefined) return undefined
95+
if (Array.isArray(data)) return [...data] as T
96+
if (typeof data === 'object') return { ...data }
97+
return data
98+
}
99+
100+
protected logDocumentChange(documentId: string | ProtectedString<any>, changeType: string): void {
101+
this._logger.silly(`${this._name} ${changeType} ${documentId}`)
102+
}
103+
104+
protected logUpdateReceived(collectionName: string, updateCount: number | undefined): void
105+
protected logUpdateReceived(collectionName: string, extraInfo?: string): void
106+
protected logUpdateReceived(
107+
collectionName: string,
108+
extraInfoOrUpdateCount: string | number | undefined | null = null
109+
): void {
110+
let message = `${this._name} received ${collectionName} update`
111+
if (typeof extraInfoOrUpdateCount === 'string') {
112+
message += `, ${extraInfoOrUpdateCount}`
113+
} else if (extraInfoOrUpdateCount !== null) {
114+
message += `(${extraInfoOrUpdateCount})`
115+
}
116+
this._logger.debug(message)
117+
}
118+
119+
protected logNotifyingUpdate(updateCount: number | undefined): void {
120+
this._logger.debug(`${this._name} notifying update with ${updateCount} ${this._collectionName}`)
121+
}
122+
123+
protected getCollectionOrFail(): CoreCollection<CollectionDocCheck<CorelibPubSubCollections[TCollection]>> {
124+
const collection = this._core.getCollection<TCollection>(this._collectionName)
125+
if (!collection) throw new Error(`collection '${this._collectionName}' not found!`)
126+
return collection
127+
}
128+
}
Lines changed: 3 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,11 @@
11
import { Logger } from 'winston'
22
import { CoreHandler } from '../coreHandler'
3-
import { CollectionBase, Collection, CollectionObserver } from '../wsHandler'
4-
import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction'
5-
import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance'
63
import { CollectionName } from '@sofie-automation/corelib/dist/dataModel/Collections'
74
import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub'
8-
import { AdLibActionId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids'
9-
import { SelectedPartInstances } from './partInstancesHandler'
10-
11-
export class AdLibActionsHandler
12-
extends CollectionBase<AdLibAction[], CorelibPubSub.adLibActions, CollectionName.AdLibActions>
13-
implements Collection<AdLibAction[]>, CollectionObserver<SelectedPartInstances>
14-
{
15-
public observerName: string
16-
private _curRundownId: RundownId | undefined
17-
private _curPartInstance: DBPartInstance | undefined
5+
import { RundownContentHandlerBase } from './rundownContentHandlerBase'
186

7+
export class AdLibActionsHandler extends RundownContentHandlerBase<CorelibPubSub.adLibActions> {
198
constructor(logger: Logger, coreHandler: CoreHandler) {
20-
super(AdLibActionsHandler.name, CollectionName.AdLibActions, CorelibPubSub.adLibActions, logger, coreHandler)
21-
this.observerName = this._name
22-
}
23-
24-
async changed(id: AdLibActionId, changeType: string): Promise<void> {
25-
this.logDocumentChange(id, changeType)
26-
if (!this._collectionName) return
27-
const col = this._core.getCollection(this._collectionName)
28-
if (!col) throw new Error(`collection '${this._collectionName}' not found!`)
29-
this._collectionData = col.find({ rundownId: this._curRundownId })
30-
await this.notify(this._collectionData)
31-
}
32-
33-
async update(source: string, data: SelectedPartInstances | undefined): Promise<void> {
34-
this.logUpdateReceived('partInstances', source)
35-
const prevRundownId = this._curRundownId
36-
this._curPartInstance = data ? data.current ?? data.next : undefined
37-
this._curRundownId = this._curPartInstance ? this._curPartInstance.rundownId : undefined
38-
39-
await new Promise(process.nextTick.bind(this))
40-
if (!this._collectionName) return
41-
if (!this._publicationName) return
42-
if (prevRundownId !== this._curRundownId) {
43-
if (this._subscriptionId) this._coreHandler.unsubscribe(this._subscriptionId)
44-
if (this._dbObserver) this._dbObserver.stop()
45-
if (this._curRundownId && this._curPartInstance) {
46-
this._subscriptionId = await this._coreHandler.setupSubscription(this._publicationName, [
47-
this._curRundownId,
48-
])
49-
this._dbObserver = this._coreHandler.setupObserver(this._collectionName)
50-
this._dbObserver.added = (id) => {
51-
void this.changed(id, 'added').catch(this._logger.error)
52-
}
53-
this._dbObserver.changed = (id) => {
54-
void this.changed(id, 'changed').catch(this._logger.error)
55-
}
56-
this._dbObserver.removed = (id) => {
57-
void this.changed(id, 'removed').catch(this._logger.error)
58-
}
59-
60-
const collection = this._core.getCollection(this._collectionName)
61-
if (!collection) throw new Error(`collection '${this._collectionName}' not found!`)
62-
this._collectionData = collection.find({
63-
rundownId: this._curRundownId,
64-
})
65-
await this.notify(this._collectionData)
66-
}
67-
}
68-
}
69-
70-
// override notify to implement empty array handling
71-
async notify(data: AdLibAction[] | undefined): Promise<void> {
72-
this.logNotifyingUpdate(data?.length)
73-
if (data !== undefined) {
74-
for (const observer of this._observers) {
75-
await observer.update(this._name, data)
76-
}
77-
}
9+
super(CollectionName.AdLibActions, CorelibPubSub.adLibActions, logger, coreHandler)
7810
}
7911
}

0 commit comments

Comments
 (0)