transport.substrate.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import BN from 'bn.js';
  2. import { MediaTransport, ChannelValidationConstraints } from './transport';
  3. import { ClassId, Class, EntityId, Entity, ClassName } from '@joystream/types/lib/versioned-store';
  4. import { InputValidationLengthConstraint } from '@joystream/types/lib/forum';
  5. import { PlainEntity, EntityCodecResolver } from '@joystream/types/lib/versioned-store/EntityCodec';
  6. import { MusicTrackType } from './schemas/music/MusicTrack';
  7. import { MusicAlbumType } from './schemas/music/MusicAlbum';
  8. import { VideoType } from './schemas/video/Video';
  9. import { ContentLicenseType } from './schemas/general/ContentLicense';
  10. import { CurationStatusType } from './schemas/general/CurationStatus';
  11. import { LanguageType } from './schemas/general/Language';
  12. import { MediaObjectType } from './schemas/general/MediaObject';
  13. import { MusicGenreType } from './schemas/music/MusicGenre';
  14. import { MusicMoodType } from './schemas/music/MusicMood';
  15. import { MusicThemeType } from './schemas/music/MusicTheme';
  16. import { PublicationStatusType } from './schemas/general/PublicationStatus';
  17. import { VideoCategoryType } from './schemas/video/VideoCategory';
  18. import { ChannelEntity } from './entities/ChannelEntity';
  19. import { ChannelId, Channel } from '@joystream/types/lib/content-working-group';
  20. import { ApiPromise } from '@polkadot/api/index';
  21. import { ApiProps } from '@polkadot/react-api/types';
  22. import { Vec } from '@polkadot/types';
  23. import { LinkageResult } from '@polkadot/types/codec/Linkage';
  24. import { ChannelCodec } from './schemas/channel/Channel';
  25. import { FeaturedContentType } from './schemas/general/FeaturedContent';
  26. import { AnyChannelId, asChannelId, AnyClassId, AnyEntityId } from './common/TypeHelpers';
  27. import { SimpleCache } from '@polkadot/joy-utils/SimpleCache';
  28. import { ValidationConstraint } from '@polkadot/joy-utils/ValidationConstraint';
  29. const FIRST_CHANNEL_ID = 1;
  30. const FIRST_CLASS_ID = 1;
  31. const FIRST_ENTITY_ID = 1;
  32. /**
  33. * There are entities that refer to other entities.
  34. */
  35. const ClassNamesThatRequireLoadingInternals: ClassName[] = [
  36. 'Video',
  37. 'MusicTrack',
  38. 'MusicAlbum'
  39. ]
  40. /**
  41. * There are such group of entities that are safe to cache
  42. * becase they serve as utility entities.
  43. * Very unlikely that their values will be changed frequently.
  44. * Even if changed, this is not a big issue from UI point of view.
  45. */
  46. const ClassNamesThatCanBeCached: ClassName[] = [
  47. 'ContentLicense',
  48. 'CurationStatus',
  49. 'Language',
  50. 'MusicGenre',
  51. 'MusicMood',
  52. 'MusicTheme',
  53. 'PublicationStatus',
  54. 'VideoCategory',
  55. ]
  56. export class SubstrateTransport extends MediaTransport {
  57. protected api: ApiPromise
  58. private entityCodecResolver: EntityCodecResolver | undefined
  59. private channelCache: SimpleCache<ChannelId, ChannelEntity>
  60. private entityCache: SimpleCache<EntityId, PlainEntity>
  61. private classCache: SimpleCache<ClassId, Class>
  62. // Ids of such entities as Language, Video Category, Music Mood, etc
  63. // will be pushed to this array later in this transport class.
  64. private idsOfEntitiesToKeepInCache: Set<string> = new Set()
  65. constructor(api: ApiProps) {
  66. super();
  67. console.log('Create new SubstrateTransport')
  68. if (!api) {
  69. throw new Error('Cannot create SubstrateTransport: Substrate API is required');
  70. } else if (!api.isApiReady) {
  71. throw new Error('Cannot create SubstrateTransport: Substrate API is not ready yet');
  72. }
  73. this.api = api.api
  74. const loadChannelsByIds = this.loadChannelsByIds.bind(this)
  75. const loadEntitiesByIds = this.loadPlainEntitiesByIds.bind(this)
  76. const loadClassesByIds = this.loadClassesByIds.bind(this)
  77. this.channelCache = new SimpleCache('Channel Cache', loadChannelsByIds)
  78. this.entityCache = new SimpleCache('Entity Cache', loadEntitiesByIds)
  79. this.classCache = new SimpleCache('Class Cache', loadClassesByIds)
  80. }
  81. protected notImplementedYet<T> (): T {
  82. throw new Error('Substrate transport: Requested function is not implemented yet')
  83. }
  84. /** Content Working Group query. */
  85. cwgQuery() {
  86. return this.api.query.contentWorkingGroup
  87. }
  88. /** Versioned Store query. */
  89. vsQuery() {
  90. return this.api.query.versionedStore
  91. }
  92. clearSessionCache() {
  93. console.info(`Clear cache of Substrate Transport`)
  94. this.channelCache.clear()
  95. this.entityCache.clearExcept(
  96. this.idsOfEntitiesToKeepInCache
  97. )
  98. // Don't clean Class cache. It's safe to preserve it between transport sessions.
  99. // this.classCache.clear()
  100. }
  101. // Channels (Content Working Group module)
  102. // -----------------------------------------------------------------
  103. async nextChannelId(): Promise<ChannelId> {
  104. return await this.cwgQuery().nextChannelId<ChannelId>()
  105. }
  106. async allChannelIds(): Promise<ChannelId[]> {
  107. let nextId = (await this.nextChannelId()).toNumber()
  108. if (nextId < 1) nextId = 1
  109. const allIds: ChannelId[] = []
  110. for (let id = FIRST_CHANNEL_ID; id < nextId; id++) {
  111. allIds.push(new ChannelId(id))
  112. }
  113. return allIds
  114. }
  115. async loadChannelsByIds(ids: AnyChannelId[]): Promise<ChannelEntity[]> {
  116. const channelTuples = await this.cwgQuery().channelById.multi<LinkageResult>(ids)
  117. return channelTuples.map((tuple, i) => {
  118. const channel = tuple[0] as Channel
  119. const id = asChannelId(ids[i])
  120. const plain = ChannelCodec.fromSubstrate(id, channel)
  121. return {
  122. ...plain,
  123. rewardEarned: new BN(0), // TODO calc this value based on chain data
  124. contentItemsCount: 0, // TODO calc this value based on chain data
  125. }
  126. })
  127. }
  128. async allChannels(): Promise<ChannelEntity[]> {
  129. const ids = await this.allChannelIds()
  130. return await this.channelCache.getOrLoadByIds(ids)
  131. }
  132. protected async getValidationConstraint(constraintName: string): Promise<ValidationConstraint> {
  133. const constraint = await this.cwgQuery()[constraintName]<InputValidationLengthConstraint>()
  134. return {
  135. min: constraint.min.toNumber(),
  136. max: constraint.max.toNumber()
  137. }
  138. }
  139. async channelValidationConstraints(): Promise<ChannelValidationConstraints> {
  140. const [
  141. handle,
  142. title,
  143. description,
  144. avatar,
  145. banner
  146. ] = await Promise.all([
  147. this.getValidationConstraint('channelHandleConstraint'),
  148. this.getValidationConstraint('channelTitleConstraint'),
  149. this.getValidationConstraint('channelDescriptionConstraint'),
  150. this.getValidationConstraint('channelAvatarConstraint'),
  151. this.getValidationConstraint('channelBannerConstraint')
  152. ])
  153. return {
  154. handle,
  155. title,
  156. description,
  157. avatar,
  158. banner
  159. }
  160. }
  161. // Classes (Versioned Store module)
  162. // -----------------------------------------------------------------
  163. async nextClassId(): Promise<ClassId> {
  164. return await this.vsQuery().nextClassId<ClassId>()
  165. }
  166. async allClassIds(): Promise<ClassId[]> {
  167. let nextId = (await this.nextClassId()).toNumber()
  168. const allIds: ClassId[] = []
  169. for (let id = FIRST_CLASS_ID; id < nextId; id++) {
  170. allIds.push(new ClassId(id))
  171. }
  172. return allIds
  173. }
  174. async loadClassesByIds(ids: AnyClassId[]): Promise<Class[]> {
  175. return await this.vsQuery().classById.multi<Vec<Class>>(ids) as unknown as Class[]
  176. }
  177. async allClasses(): Promise<Class[]> {
  178. const ids = await this.allClassIds()
  179. return await this.classCache.getOrLoadByIds(ids)
  180. }
  181. async getEntityCodecResolver(): Promise<EntityCodecResolver> {
  182. if (!this.entityCodecResolver) {
  183. const classes = await this.allClasses()
  184. this.entityCodecResolver = new EntityCodecResolver(classes)
  185. }
  186. return this.entityCodecResolver
  187. }
  188. async classNamesToIdSet(classNames: ClassName[]): Promise<Set<string>> {
  189. const classNameToIdMap = await this.classIdByNameMap()
  190. return new Set(classNames
  191. .map(name => {
  192. const classId = classNameToIdMap[name]
  193. return classId ? classId.toString() : undefined
  194. })
  195. .filter(classId => typeof classId !== 'undefined') as string[]
  196. )
  197. }
  198. // Entities (Versioned Store module)
  199. // -----------------------------------------------------------------
  200. async nextEntityId(): Promise<EntityId> {
  201. return await this.vsQuery().nextEntityId<EntityId>()
  202. }
  203. async allEntityIds(): Promise<EntityId[]> {
  204. let nextId = (await this.nextEntityId()).toNumber()
  205. const allIds: EntityId[] = []
  206. for (let id = FIRST_ENTITY_ID; id < nextId; id++) {
  207. allIds.push(new EntityId(id))
  208. }
  209. return allIds
  210. }
  211. private async loadEntitiesByIds(ids: AnyEntityId[]): Promise<Entity[]> {
  212. if (!ids || ids.length === 0) return []
  213. return await this.vsQuery().entityById.multi<Vec<Entity>>(ids) as unknown as Entity[]
  214. }
  215. // TODO try to cache this func
  216. private async loadPlainEntitiesByIds(ids: AnyEntityId[]): Promise<PlainEntity[]> {
  217. const entities = await this.loadEntitiesByIds(ids)
  218. const cacheClassIds = await this.classNamesToIdSet(ClassNamesThatCanBeCached)
  219. entities.forEach(e => {
  220. if (cacheClassIds.has(e.class_id.toString())) {
  221. this.idsOfEntitiesToKeepInCache.add(e.id.toString())
  222. }
  223. })
  224. // Next logs are usefull for debug:
  225. // console.log('cacheClassIds', cacheClassIds)
  226. // console.log('idsOfEntitiesToKeepInCache', this.idsOfEntitiesToKeepInCache)
  227. return await this.toPlainEntitiesAndResolveInternals(entities)
  228. }
  229. async allPlainEntities(): Promise<PlainEntity[]> {
  230. const ids = await this.allEntityIds()
  231. return await this.entityCache.getOrLoadByIds(ids)
  232. }
  233. async findPlainEntitiesByClassName<T extends PlainEntity> (className: ClassName): Promise<T[]> {
  234. const res: T[] = []
  235. const clazz = await this.classByName(className)
  236. if (!clazz) {
  237. console.warn(`No class found by name '${className}'`)
  238. return res
  239. }
  240. const allIds = await this.allEntityIds()
  241. const filteredEntities = (await this.entityCache.getOrLoadByIds(allIds))
  242. .filter(entity => clazz.id.eq(entity.classId)) as T[]
  243. console.log(`Found ${filteredEntities.length} plain entities by class name '${className}'`)
  244. return filteredEntities
  245. }
  246. async toPlainEntitiesAndResolveInternals(entities: Entity[]): Promise<PlainEntity[]> {
  247. const loadEntityById = this.entityCache.getOrLoadById.bind(this.entityCache)
  248. const loadChannelById = this.channelCache.getOrLoadById.bind(this.channelCache)
  249. const entityCodecResolver = await this.getEntityCodecResolver()
  250. const loadableClassIds = await this.classNamesToIdSet(ClassNamesThatRequireLoadingInternals)
  251. const convertions: Promise<PlainEntity>[] = []
  252. for (const entity of entities) {
  253. const classIdStr = entity.class_id.toString()
  254. const codec = entityCodecResolver.getCodecByClassId(entity.class_id)
  255. if (!codec) {
  256. console.warn(`No entity codec found by class id: ${classIdStr}`)
  257. continue
  258. }
  259. const loadInternals = loadableClassIds.has(classIdStr)
  260. convertions.push(
  261. codec.toPlainObject(
  262. entity, {
  263. loadInternals,
  264. loadEntityById,
  265. loadChannelById
  266. }))
  267. }
  268. return Promise.all(convertions)
  269. }
  270. // Load entities by class name:
  271. // -----------------------------------------------------------------
  272. async featuredContent(): Promise<FeaturedContentType | undefined> {
  273. const arr = await this.findPlainEntitiesByClassName('FeaturedContent')
  274. return arr && arr.length ? arr[0] : undefined
  275. }
  276. async allMediaObjects(): Promise<MediaObjectType[]> {
  277. return await this.findPlainEntitiesByClassName('MediaObject')
  278. }
  279. async allVideos(): Promise<VideoType[]> {
  280. return await this.findPlainEntitiesByClassName('Video')
  281. }
  282. async allMusicTracks(): Promise<MusicTrackType[]> {
  283. return await this.findPlainEntitiesByClassName('MusicTrack')
  284. }
  285. async allMusicAlbums(): Promise<MusicAlbumType[]> {
  286. return await this.findPlainEntitiesByClassName('MusicAlbum')
  287. }
  288. async allContentLicenses (): Promise<ContentLicenseType[]> {
  289. return await this.findPlainEntitiesByClassName('ContentLicense')
  290. }
  291. async allCurationStatuses(): Promise<CurationStatusType[]> {
  292. return await this.findPlainEntitiesByClassName('CurationStatus')
  293. }
  294. async allLanguages(): Promise<LanguageType[]> {
  295. return await this.findPlainEntitiesByClassName('Language')
  296. }
  297. async allMusicGenres(): Promise<MusicGenreType[]> {
  298. return await this.findPlainEntitiesByClassName('MusicGenre')
  299. }
  300. async allMusicMoods(): Promise<MusicMoodType[]> {
  301. return await this.findPlainEntitiesByClassName('MusicMood')
  302. }
  303. async allMusicThemes(): Promise<MusicThemeType[]> {
  304. return await this.findPlainEntitiesByClassName('MusicTheme')
  305. }
  306. async allPublicationStatuses(): Promise<PublicationStatusType[]> {
  307. return await this.findPlainEntitiesByClassName('PublicationStatus')
  308. }
  309. async allVideoCategories(): Promise<VideoCategoryType[]> {
  310. return await this.findPlainEntitiesByClassName('VideoCategory')
  311. }
  312. }