activeVideoCounters.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import { DerivedPropertiesManager } from '../classes'
  2. import { IExecutor, IListener, IChangePair } from '../interfaces'
  3. import { DatabaseManager } from '@joystream/hydra-common'
  4. import { Channel, ChannelCategory, Video, VideoCategory, StorageDataObject } from 'query-node/dist/model'
  5. import { videoRelationsForCountersBare } from '../../content/utils'
  6. export type IVideoDerivedEntites = 'channel' | 'channel.category' | 'category'
  7. export type IAvcChange = 1 | -1 | [1 | -1, IVideoDerivedEntites[]]
  8. export type IAvcChannelChange = number
  9. /*
  10. Decides if video is considered active.
  11. */
  12. function isVideoActive(video: Video): boolean {
  13. return !!video.isPublic && !video.isCensored && !!video.thumbnailPhoto?.isAccepted && !!video.media?.isAccepted
  14. }
  15. /*
  16. Compares original and updated videos and calculates if video active status changed.
  17. */
  18. function hasVideoChanged(
  19. oldValue: Video | undefined,
  20. newValue: Video | undefined
  21. ): IChangePair<IAvcChange> | undefined {
  22. // at least one video should always exists but due to TS type limitations
  23. // (can't define at least one-of-two parameters required) this safety condition needs to be here
  24. if (!oldValue && !newValue) {
  25. return undefined
  26. }
  27. // video is being created?
  28. if (!oldValue) {
  29. return {
  30. old: undefined,
  31. new: (Number(isVideoActive(newValue as Video)) as IAvcChange) || undefined,
  32. }
  33. }
  34. // video is being deleted?
  35. if (!newValue) {
  36. return {
  37. old: (-Number(isVideoActive(oldValue)) as IAvcChange) || undefined,
  38. new: undefined,
  39. }
  40. }
  41. // calculate active status
  42. const originalState = isVideoActive(oldValue)
  43. const newState = isVideoActive(newValue)
  44. // escape if no change and video is not active
  45. if (originalState === newState && !newState) {
  46. return undefined
  47. }
  48. // active status stays unchanged but relation(s) changed, return list of changed relations
  49. if (originalState === newState) {
  50. return {
  51. old: [
  52. -1,
  53. [
  54. oldValue.channel && oldValue.channel.id !== newValue.channel?.id && 'channel',
  55. oldValue.channel?.category &&
  56. oldValue.channel.category?.id !== newValue.channel?.category?.id &&
  57. 'channel.category',
  58. oldValue.category && oldValue.category.id !== newValue.category?.id && 'category',
  59. ].filter((item) => item) as IVideoDerivedEntites[],
  60. ],
  61. new: [
  62. 1,
  63. [
  64. newValue.channel && oldValue.channel?.id !== newValue.channel.id && 'channel',
  65. newValue.channel?.category &&
  66. oldValue.channel?.category?.id !== newValue.channel.category?.id &&
  67. 'channel.category',
  68. newValue.category && oldValue.category?.id !== newValue.category.id && 'category',
  69. ].filter((item) => item) as IVideoDerivedEntites[],
  70. ],
  71. }
  72. }
  73. // calculate change
  74. const change = Number(newState) - Number(originalState)
  75. return {
  76. old: (-change as IAvcChange) || undefined,
  77. new: (change as IAvcChange) || undefined,
  78. }
  79. }
  80. /*
  81. Listener for video events.
  82. */
  83. class VideoUpdateListener implements IListener<Video, IAvcChange> {
  84. getRelationDependencies(): string[] {
  85. return ['thumbnailPhoto', 'media']
  86. }
  87. hasValueChanged(oldValue: Video | undefined, newValue: Video): IChangePair<IAvcChange> | undefined
  88. hasValueChanged(oldValue: Video, newValue: Video | undefined): IChangePair<IAvcChange> | undefined
  89. hasValueChanged(oldValue: Video, newValue: Video): IChangePair<IAvcChange> | undefined {
  90. return hasVideoChanged(oldValue, newValue)
  91. }
  92. }
  93. /*
  94. Listener for channel's category update.
  95. */
  96. class ChannelsCategoryChangeListener implements IListener<Channel, IAvcChannelChange> {
  97. getRelationDependencies(): string[] {
  98. return ['category']
  99. }
  100. hasValueChanged(oldValue: Channel | undefined, newValue: Channel): IChangePair<IAvcChannelChange> | undefined
  101. hasValueChanged(oldValue: Channel, newValue: Channel | undefined): IChangePair<IAvcChannelChange> | undefined
  102. hasValueChanged(oldValue: Channel, newValue: Channel): IChangePair<IAvcChannelChange> | undefined {
  103. return {
  104. old: -oldValue?.activeVideosCounter || undefined,
  105. new: newValue?.activeVideosCounter || undefined,
  106. }
  107. }
  108. }
  109. /*
  110. Listener for thumbnail photo events.
  111. */
  112. class StorageDataObjectChangeListener_ThumbnailPhoto implements IListener<StorageDataObject, IAvcChange> {
  113. getRelationDependencies(): string[] {
  114. return [
  115. 'videoThumbnail',
  116. 'videoThumbnail.thumbnailPhoto',
  117. 'videoThumbnail.media',
  118. 'videoThumbnail.category',
  119. 'videoThumbnail.channel',
  120. 'videoThumbnail.channel.category',
  121. ]
  122. }
  123. hasValueChanged(
  124. oldValue: StorageDataObject | undefined,
  125. newValue: StorageDataObject
  126. ): IChangePair<IAvcChange> | undefined
  127. hasValueChanged(
  128. oldValue: StorageDataObject,
  129. newValue: StorageDataObject | undefined
  130. ): IChangePair<IAvcChange> | undefined
  131. hasValueChanged(oldValue: StorageDataObject, newValue: StorageDataObject): IChangePair<IAvcChange> | undefined {
  132. const oldVideo = oldValue?.videoThumbnail
  133. const newVideo = newValue?.videoThumbnail
  134. return hasVideoChanged(oldVideo, newVideo)
  135. }
  136. }
  137. /*
  138. Listener for media events.
  139. */
  140. class StorageDataObjectChangeListener_Media implements IListener<StorageDataObject, IAvcChange> {
  141. getRelationDependencies(): string[] {
  142. return [
  143. 'videoMedia',
  144. 'videoMedia.thumbnailPhoto',
  145. 'videoMedia.media',
  146. 'videoMedia.category',
  147. 'videoMedia.channel',
  148. 'videoMedia.channel.category',
  149. ]
  150. }
  151. hasValueChanged(
  152. oldValue: StorageDataObject | undefined,
  153. newValue: StorageDataObject
  154. ): IChangePair<IAvcChange> | undefined
  155. hasValueChanged(
  156. oldValue: StorageDataObject,
  157. newValue: StorageDataObject | undefined
  158. ): IChangePair<IAvcChange> | undefined
  159. hasValueChanged(oldValue: StorageDataObject, newValue: StorageDataObject): IChangePair<IAvcChange> | undefined {
  160. const oldVideo = oldValue?.videoMedia
  161. const newVideo = newValue?.videoMedia
  162. return hasVideoChanged(oldVideo as Video, newVideo as Video)
  163. }
  164. }
  165. /*
  166. Adapter for generalizing AVC executor.
  167. */
  168. interface IAvcExecutorAdapter<Entity> {
  169. (item: Entity): Video
  170. }
  171. /*
  172. Active video counter executor reflecting changes to channels, channel cateories, or video categories.
  173. */
  174. class ActiveVideoCounterExecutor<
  175. Entity extends Video | StorageDataObject,
  176. DerivedEntity extends VideoCategory | Channel | ChannelCategory = VideoCategory | Channel | ChannelCategory
  177. > implements IExecutor<Entity, IAvcChange, DerivedEntity> {
  178. private adapter: IAvcExecutorAdapter<Entity>
  179. constructor(adapter: IAvcExecutorAdapter<Entity>) {
  180. this.adapter = adapter
  181. }
  182. async loadDerivedEntities(store: DatabaseManager, entity: Entity): Promise<DerivedEntity[]> {
  183. // TODO: find way to reliably decide if channel, etc. are loaded and throw error if not
  184. const targetEntity = this.adapter(entity)
  185. // this expects entity has loaded channel, channel category, and video category
  186. return [targetEntity.channel, targetEntity.channel?.category, targetEntity.category].filter(
  187. (item) => item
  188. ) as DerivedEntity[]
  189. }
  190. async saveDerivedEntities(store: DatabaseManager, entities: DerivedEntity[]): Promise<void> {
  191. await Promise.all(entities.map((entity) => store.save(entity)))
  192. }
  193. updateOldValue(entity: DerivedEntity, change: IAvcChange): DerivedEntity {
  194. entity = this.updateValueCommon(entity, change)
  195. return entity
  196. }
  197. updateNewValue(entity: DerivedEntity, change: IAvcChange): DerivedEntity {
  198. entity = this.updateValueCommon(entity, change)
  199. return entity
  200. }
  201. private updateValueCommon(entity: DerivedEntity, change: IAvcChange): DerivedEntity {
  202. if (typeof change === 'number') {
  203. entity.activeVideosCounter += change
  204. return entity
  205. }
  206. const [counterChange, entitiesToChange] = change
  207. const shouldChange =
  208. false ||
  209. (entity instanceof Channel && entitiesToChange.includes('channel')) ||
  210. (entity instanceof ChannelCategory && entitiesToChange.includes('channel.category')) ||
  211. (entity instanceof VideoCategory && entitiesToChange.includes('category'))
  212. if (shouldChange) {
  213. entity.activeVideosCounter += counterChange
  214. }
  215. return entity
  216. }
  217. }
  218. /*
  219. Executor reflecting changes to channel's category.
  220. */
  221. class ChannelCategoryActiveVideoCounterExecutor implements IExecutor<Channel, IAvcChannelChange, ChannelCategory> {
  222. async loadDerivedEntities(store: DatabaseManager, channel: Channel): Promise<ChannelCategory[]> {
  223. // TODO: find way to reliably decide if channel, etc. are loaded and throw error if not
  224. // this expects entity has category
  225. return [channel.category].filter((item) => item) as ChannelCategory[]
  226. }
  227. async saveDerivedEntities(store: DatabaseManager, [entity]: ChannelCategory[]): Promise<void> {
  228. await store.save(entity)
  229. }
  230. updateOldValue(entity: ChannelCategory, change: IAvcChannelChange): ChannelCategory {
  231. entity.activeVideosCounter += change
  232. return entity
  233. }
  234. updateNewValue(entity: ChannelCategory, change: IAvcChannelChange): ChannelCategory {
  235. entity.activeVideosCounter += change
  236. return entity
  237. }
  238. }
  239. export function createVideoManager(store: DatabaseManager): DerivedPropertiesManager<Video, IAvcChange> {
  240. const manager = new DerivedPropertiesManager<Video, IAvcChange>(store, Video, videoRelationsForCountersBare)
  241. // listen to video change
  242. const listener = new VideoUpdateListener()
  243. const executors = [new ActiveVideoCounterExecutor<Video>((video) => video)]
  244. manager.registerListener(listener, executors)
  245. return manager
  246. }
  247. export function createChannelManager(store: DatabaseManager): DerivedPropertiesManager<Channel, IAvcChannelChange> {
  248. const manager = new DerivedPropertiesManager<Channel, IAvcChannelChange>(store, Channel)
  249. // listen to change of channel's category
  250. const channelListener = new ChannelsCategoryChangeListener()
  251. const channelExecutors = [new ChannelCategoryActiveVideoCounterExecutor()]
  252. manager.registerListener(channelListener, channelExecutors)
  253. return manager
  254. }
  255. export function createStorageDataObjectManager(
  256. store: DatabaseManager
  257. ): DerivedPropertiesManager<StorageDataObject, IAvcChange> {
  258. const manager = new DerivedPropertiesManager<StorageDataObject, IAvcChange>(store, StorageDataObject)
  259. // listen to change of channel's category
  260. const storageDataObjectListener1 = new StorageDataObjectChangeListener_ThumbnailPhoto()
  261. const storageDataObjectListener2 = new StorageDataObjectChangeListener_Media()
  262. const storageDataObjectExecutors = (adapter) => [new ActiveVideoCounterExecutor<StorageDataObject>(adapter)]
  263. manager.registerListener(
  264. storageDataObjectListener1,
  265. storageDataObjectExecutors((storageDataObject) => storageDataObject.videoThumbnail)
  266. )
  267. manager.registerListener(
  268. storageDataObjectListener2,
  269. storageDataObjectExecutors((storageDataObject) => storageDataObject.videoMedia)
  270. )
  271. return manager
  272. }