Skip to content

Commit a7deec9

Browse files
committed
Merge branch 'release51'
2 parents 9aa62dd + 4253eea commit a7deec9

File tree

33 files changed

+567
-273
lines changed

33 files changed

+567
-273
lines changed

.github/workflows/node.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,6 @@ jobs:
748748
CI: true
749749
- name: Run check
750750
run: |
751-
node scripts/checkForMultipleVersions.mjs
751+
yarn validate:versions
752752
env:
753753
CI: true

meteor/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [1.51.5](///compare/v1.51.4...v1.51.5) (2025-01-07)
6+
7+
8+
### Bug Fixes
9+
10+
* **job-worker/playout:** Hold mode doesn't work at all a7d6999
11+
12+
### [1.51.4](///compare/v1.51.3...v1.51.4) (2024-12-04)
13+
14+
15+
### Bug Fixes
16+
17+
* Device Action Studio Context gets lost, Adlib previews are unstable 193815d
18+
* Live Status Gateway Dockerfile (regular) still uses yarn to start 0ae53c4
19+
* release scripts broken on Windows 9636051
20+
* RundownView shows spinner when unMOSing a Rundown from a Playlist 874e85c
21+
522
### [1.51.2](https://github.com/nrkno/tv-automation-server-core/compare/v1.51.1...v1.51.2) (2024-11-21)
623

724

meteor/client/lib/ReactMeteorData/ReactMeteorData.tsx

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable react/prefer-stateless-function */
22

3-
import React, { useState, useEffect, useRef } from 'react'
3+
import React, { useState, useEffect, useRef, useCallback } from 'react'
44
import { Meteor } from 'meteor/meteor'
55
import { Mongo } from 'meteor/mongo'
66
import { Tracker } from 'meteor/tracker'
@@ -13,6 +13,8 @@ const globalTrackerQueue: Array<Function> = []
1313
let globalTrackerTimestamp: number | undefined = undefined
1414
let globalTrackerTimeout: number | undefined = undefined
1515

16+
const SUBSCRIPTION_TIMEOUT = 1000
17+
1618
/**
1719
* Delay an update to be batched with the global tracker invalidation queue
1820
*/
@@ -370,6 +372,61 @@ export function useTracker<T, K extends undefined | T = undefined>(
370372
return meteorData
371373
}
372374

375+
/**
376+
* A hook to track a boolean state with a sort of histeresis, with preference for `true`. `setState` makes the returned
377+
* `state` be `true` immediately, but `false` only after `resetState` is called and `timeout` elapses. If `setState`
378+
* is called with `true` before `timeout` elapses, then `resetState` is aborted and `state` will remain `ture.
379+
*
380+
* Later `resetState` calls replace earlier unelapsed calls and their timeout periods.
381+
*
382+
* @param {boolean} [initialState=false]
383+
* @return {*} {{
384+
* state: boolean
385+
* setState: (value: boolean) => void
386+
* resetState: (timeout: number) => void
387+
* }}
388+
*/
389+
function useDelayState(initialState = false): {
390+
state: boolean
391+
setState: (value: boolean) => void
392+
resetState: (timeout: number) => void
393+
} {
394+
const [state, setState] = useState(initialState)
395+
const [prevState, setPrevState] = useState(initialState)
396+
const prevReadyTimeoutRef = useRef<number | null>(null)
397+
398+
const setStateAndClearResets = useCallback(
399+
(value: boolean) => {
400+
setState(value)
401+
402+
if (value) {
403+
setPrevState(true)
404+
if (prevReadyTimeoutRef.current !== null) {
405+
window.clearTimeout(prevReadyTimeoutRef.current)
406+
prevReadyTimeoutRef.current = null
407+
}
408+
}
409+
},
410+
[setState, setPrevState]
411+
)
412+
413+
const resetStateAfterDelay = useCallback((timeout: number) => {
414+
if (prevReadyTimeoutRef.current !== null) {
415+
window.clearTimeout(prevReadyTimeoutRef.current)
416+
}
417+
prevReadyTimeoutRef.current = window.setTimeout(() => {
418+
prevReadyTimeoutRef.current = null
419+
setPrevState(false)
420+
}, timeout)
421+
}, [])
422+
423+
return {
424+
state: state || prevState,
425+
setState: setStateAndClearResets,
426+
resetState: resetStateAfterDelay,
427+
}
428+
}
429+
373430
/**
374431
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
375432
* Subscriptions will be torn down 1000ms after unmounting the component.
@@ -383,20 +440,28 @@ export function useSubscription<K extends keyof AllPubSubTypes>(
383440
sub: K,
384441
...args: Parameters<AllPubSubTypes[K]>
385442
): boolean {
386-
const [ready, setReady] = useState<boolean>(false)
443+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
387444

388445
useEffect(() => {
389446
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
390-
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
447+
const isReadyComp = Tracker.nonreactive(() =>
448+
Tracker.autorun(() => {
449+
const isNowReady = subscription.ready()
450+
setReady(isNowReady)
451+
})
452+
)
391453
return () => {
392454
isReadyComp.stop()
393455
setTimeout(() => {
394456
subscription.stop()
395-
}, 1000)
457+
}, SUBSCRIPTION_TIMEOUT)
458+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
396459
}
397460
}, [sub, stringifyObjects(args)])
398461

399-
return ready
462+
const isReady = ready
463+
464+
return isReady
400465
}
401466

402467
/**
@@ -415,7 +480,7 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
415480
enable: boolean,
416481
...args: Parameters<AllPubSubTypes[K]>
417482
): boolean {
418-
const [ready, setReady] = useState<boolean>(false)
483+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
419484

420485
useEffect(() => {
421486
if (!enable) {
@@ -424,16 +489,69 @@ export function useSubscriptionIfEnabled<K extends keyof AllPubSubTypes>(
424489
}
425490

426491
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
427-
const isReadyComp = Tracker.nonreactive(() => Tracker.autorun(() => setReady(subscription.ready())))
492+
const isReadyComp = Tracker.nonreactive(() =>
493+
Tracker.autorun(() => {
494+
const isNowReady = subscription.ready()
495+
setReady(isNowReady)
496+
})
497+
)
428498
return () => {
429499
isReadyComp.stop()
430500
setTimeout(() => {
431501
subscription.stop()
432-
}, 1000)
502+
}, SUBSCRIPTION_TIMEOUT)
503+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
433504
}
434505
}, [sub, enable, stringifyObjects(args)])
435506

436-
return !enable || ready
507+
const isReady = !enable || ready
508+
509+
return isReady
510+
}
511+
512+
/**
513+
* A Meteor Subscription hook that allows using React Functional Components and the Hooks API with Meteor subscriptions.
514+
* Subscriptions will be torn down 1000ms after unmounting the component.
515+
* If the subscription is not enabled, the subscription will not be created, and the ready state will always be true.
516+
*
517+
* @export
518+
* @param {PubSub} sub The subscription to be subscribed to
519+
* @param {boolean} enable Whether the subscription is enabled
520+
* @param {...any[]} args A list of arugments for the subscription. This is used for optimizing the subscription across
521+
* renders so that it isn't torn down and created for every render.
522+
*/
523+
export function useSubscriptionIfEnabledReadyOnce<K extends keyof AllPubSubTypes>(
524+
sub: K,
525+
enable: boolean,
526+
...args: Parameters<AllPubSubTypes[K]>
527+
): boolean {
528+
const { state: ready, setState: setReady, resetState: cancelPreviousReady } = useDelayState()
529+
530+
useEffect(() => {
531+
if (!enable) {
532+
setReady(false)
533+
return
534+
}
535+
536+
const subscription = Tracker.nonreactive(() => meteorSubscribe(sub, ...args))
537+
const isReadyComp = Tracker.nonreactive(() =>
538+
Tracker.autorun(() => {
539+
const isNowReady = subscription.ready()
540+
if (isNowReady) setReady(true)
541+
})
542+
)
543+
return () => {
544+
isReadyComp.stop()
545+
setTimeout(() => {
546+
subscription.stop()
547+
}, SUBSCRIPTION_TIMEOUT)
548+
cancelPreviousReady(SUBSCRIPTION_TIMEOUT)
549+
}
550+
}, [sub, enable, stringifyObjects(args)])
551+
552+
const isReady = !enable || ready
553+
554+
return isReady
437555
}
438556

439557
/**

meteor/client/ui/RundownView.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Translated,
88
translateWithTracker,
99
useSubscriptionIfEnabled,
10+
useSubscriptionIfEnabledReadyOnce,
1011
useSubscriptions,
1112
useTracker,
1213
} from '../lib/ReactMeteorData/react-meteor-data'
@@ -1227,9 +1228,6 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12271228

12281229
return playlist?.studioId
12291230
}, [playlistId])
1230-
// Load once the playlist is confirmed to exist
1231-
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
1232-
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))
12331231
// Load only when the studio is known
12341232
requiredSubsReady.push(
12351233
useSubscriptionIfEnabled(MeteorPubSub.uiStudio, !!playlistStudioId, playlistStudioId ?? protectString(''))
@@ -1258,7 +1256,12 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12581256
)
12591257
)
12601258
requiredSubsReady.push(
1261-
useSubscriptionIfEnabled(CorelibPubSub.showStyleVariants, showStyleVariantIds.length > 0, null, showStyleVariantIds)
1259+
useSubscriptionIfEnabledReadyOnce(
1260+
CorelibPubSub.showStyleVariants,
1261+
showStyleVariantIds.length > 0,
1262+
null,
1263+
showStyleVariantIds
1264+
)
12621265
)
12631266
auxSubsReady.push(
12641267
useSubscriptionIfEnabled(MeteorPubSub.rundownLayouts, showStyleBaseIds.length > 0, showStyleBaseIds)
@@ -1283,6 +1286,10 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
12831286
)
12841287
)
12851288

1289+
// Load once the playlist is confirmed to exist
1290+
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiSegmentPartNotes, !!playlistStudioId, playlistId))
1291+
auxSubsReady.push(useSubscriptionIfEnabled(MeteorPubSub.uiPieceContentStatuses, !!playlistStudioId, playlistId))
1292+
12861293
useTracker(() => {
12871294
const playlist = RundownPlaylists.findOne(playlistId, {
12881295
fields: {

meteor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "automation-core",
3-
"version": "1.51.3",
3+
"version": "1.51.5",
44
"private": true,
55
"engines": {
66
"node": ">=14.19.1"

meteor/server/api/deviceTriggers/RundownContentObserver.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class RundownContentObserver {
3232
#observers: Meteor.LiveQueryHandle[] = []
3333
#cache: ContentCache
3434
#cancelCache: () => void
35-
#cleanup: () => void = () => {
35+
#cleanup: (() => void) | undefined = () => {
3636
throw new Error('RundownContentObserver.#cleanup has not been set!')
3737
}
3838
#disposed = false
@@ -45,8 +45,11 @@ export class RundownContentObserver {
4545
) {
4646
logger.silly(`Creating RundownContentObserver for playlist "${rundownPlaylistId}"`)
4747
const { cache, cancel: cancelCache } = createReactiveContentCache(() => {
48+
if (this.#disposed) {
49+
this.#cleanup?.()
50+
return
51+
}
4852
this.#cleanup = onChanged(cache)
49-
if (this.#disposed) this.#cleanup()
5053
}, REACTIVITY_DEBOUNCE)
5154

5255
this.#cache = cache
@@ -157,5 +160,6 @@ export class RundownContentObserver {
157160
this.#cancelCache()
158161
this.#observers.forEach((observer) => observer.stop())
159162
this.#cleanup?.()
163+
this.#cleanup = undefined
160164
}
161165
}

0 commit comments

Comments
 (0)