Skip to content
This repository has been archived by the owner on Dec 29, 2020. It is now read-only.

Commit

Permalink
lazy loading working
Browse files Browse the repository at this point in the history
  • Loading branch information
Rohanator9000 committed Dec 3, 2020
1 parent c420703 commit cb2aa64
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 58 deletions.
83 changes: 40 additions & 43 deletions server/src/entities/Party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,24 @@ export class Party extends BaseEntity {
@UpdateDateColumn()
latestTime: Date

@ManyToOne(() => Song, { eager: true, nullable: true })
currentSong: Song | null
@ManyToOne(() => Song, { lazy: true, nullable: true })
currentSong: Promise<Song | null>

@OneToMany(() => VotedSong, votedSong => votedSong.party, { eager: true, cascade: ['remove'] })
votedSongs: VotedSong[]
@Column({ nullable: true })
currentSongId: number

@OneToMany(() => PlayedSong, playedSong => playedSong.party, { eager: true, cascade: ['remove'] })
playedSongs: PlayedSong[]
@OneToMany(() => VotedSong, votedSong => votedSong.party, { lazy: true, cascade: ['remove'] })
votedSongs: Promise<VotedSong[]>

@OneToMany(() => PlayedSong, playedSong => playedSong.party, { lazy: true, cascade: ['remove'] })
playedSongs: Promise<PlayedSong[]>

constructor(name: string, password?: string) {
super()

this.name = name
this.password = password || null
this.currentSong = null
this.currentSong = Promise.resolve(null)
// Don't initialize this.playedSongs or this.votedSongs.
// this.playedSongs = []
// this.votedSongs = []
Expand All @@ -55,7 +58,7 @@ export class Party extends BaseEntity {
}

public async voteForSong(song: Song) {
let votedSong = await VotedSong.findOne({ song: song, party: this })
let votedSong = await VotedSong.findOne({ where: { songId: song.id, partyId: this.id } })

if (votedSong) {
await votedSong.incrementVote()
Expand All @@ -68,10 +71,14 @@ export class Party extends BaseEntity {
}

private async removeCurrentSong() {
if (this.currentSong) {
const newPlayedSong = new PlayedSong(this.currentSong, this, await this.getNextSequenceNumber())
this.currentSong = null
this.playedSongs.push(newPlayedSong)
const currentSong = await this.currentSong

if (currentSong) {
const newPlayedSong = new PlayedSong(currentSong, this, await this.getNextSequenceNumber())
this.currentSong = Promise.resolve(null)
const playedSongs = await this.playedSongs
playedSongs.push(newPlayedSong)
this.playedSongs = Promise.resolve(playedSongs)
await Promise.all([newPlayedSong.save(), this.save()])
}
}
Expand All @@ -80,50 +87,40 @@ export class Party extends BaseEntity {
const highestVotedSong = await this.getHighestVotedSong()

if (highestVotedSong) {
this.currentSong = highestVotedSong.song
this.currentSongId = (await highestVotedSong.getSong()).id
await Promise.all([highestVotedSong.remove(), this.save()])
}
}

public async getSortedVotedSongs() {
const votedSongs = await this.votedSongs
const songs = await Promise.all(votedSongs.map(votedSong => votedSong.getSong()))
const zipped: [VotedSong, Song][] = votedSongs.map((votedSong, index) => {
return [votedSong, songs[index]]
})
zipped.sort(([votedSong1, song1], [votedSong2, song2]) => {
if (votedSong1.count != votedSong2.count) {
return votedSong2.count - votedSong1.count
}
if (song1.title < song2.title) {
return -1
}
return 0
})
return zipped.map(([votedSong, _song]) => votedSong)
}

private async getHighestVotedSong() {
// TODO: TypeORM can't sort by fields of eagerly joined rows. So we currently retrieve all VotedSong for the party
// and then sort by song name and then sort by name among the VotedSong with the highest equivalent count and then
// return the first VotedSong. Optimize this by using the TypeORM query builder to build an SQL query that can
// JOIN and then order correctly to retrieve a single VotedSong rather than all VotedSong for the party.
return VotedSong.find({ where: { party: this }, order: { count: 'DESC' } }).then(votedSongs => {
if (!Array.isArray(votedSongs) || !votedSongs.length) {
return null
} else {
const highestCount = votedSongs[0].count
return votedSongs
.filter(votedSong => votedSong.count == highestCount)
.sort((votedSong1, votedSong2) => {
if (votedSong1.count != votedSong2.count) {
return votedSong2.count - votedSong1.count
}
if (votedSong1.song.title < votedSong2.song.title) {
return -1
}
return 0
})[0]
}
})
const sortedVotedSongs = await this.getSortedVotedSongs()
return sortedVotedSongs[0]
}

private async getNextSequenceNumber() {
const latestSong = await PlayedSong.findOne({ where: { party: this }, order: { sequenceNumber: 'DESC' } })
return latestSong ? latestSong.sequenceNumber + 1 : 0
}

public sortVotedSongs() {
this.votedSongs.sort((votedSong1, votedSong2) => {
if (votedSong1.count != votedSong2.count) {
return votedSong2.count - votedSong1.count
}
if (votedSong1.song.title < votedSong2.song.title) {
return -1
}
return 0
})
}
}
28 changes: 22 additions & 6 deletions server/src/entities/PlayedSong.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,33 @@ export class PlayedSong extends BaseEntity {
@Column()
sequenceNumber: number

@ManyToOne(() => Song, { eager: true, nullable: false })
song: Song
@ManyToOne(() => Song, { lazy: true, nullable: false })
song: Promise<Song>

@ManyToOne(() => Party, party => party.playedSongs, { nullable: false })
party: Party
@Column()
songId: number

@ManyToOne(() => Party, party => party.playedSongs, { nullable: false, lazy: true })
party: Promise<Party>

@Column()
partyId: number

constructor(song: Song, party: Party, sequenceNumber: number) {
super()

this.song = song
this.party = party
this.song = Promise.resolve(song)
this.party = Promise.resolve(party)
this.sequenceNumber = sequenceNumber
}

// For some reason the Song or Party fields return Promise<undefined> (I think a TypeORM bug), so we need the following two methods.
// Piazza question: https://piazza.com/class/kfpm567u1e24eb?cid=123.
public async getParty() {
return Party.findOneOrFail(this.partyId)
}

public async getSong() {
return Song.findOneOrFail(this.songId)
}
}
28 changes: 22 additions & 6 deletions server/src/entities/VotedSong.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,37 @@ export class VotedSong extends BaseEntity {
@Column()
count: number

@ManyToOne(() => Song, { eager: true })
song: Song
@ManyToOne(() => Song, { lazy: true, nullable: false })
song: Promise<Song>

@ManyToOne(() => Party, party => party.votedSongs)
party: Party
@Column()
songId: number

@ManyToOne(() => Party, party => party.votedSongs, { nullable: false, lazy: true })
party: Promise<Party>

@Column()
partyId: number

constructor(song: Song, party: Party) {
super()
this.song = song
this.party = party
this.song = Promise.resolve(song)
this.party = Promise.resolve(party)
this.count = 1
}

public async incrementVote() {
++this.count
await this.save()
}

// For some reason the Song or Party fields return Promise<undefined> (I think a TypeORM bug), so we need the following two methods.
// Piazza question: https://piazza.com/class/kfpm567u1e24eb?cid=123.
public async getParty() {
return Party.findOneOrFail(this.partyId)
}

public async getSong() {
return Song.findOneOrFail(this.songId)
}
}
26 changes: 23 additions & 3 deletions server/src/graphql/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'
import { PubSub } from 'graphql-yoga'
import path from 'path'
import { Party } from '../entities/Party'
import { PlayedSong } from '../entities/PlayedSong'
import { Song } from '../entities/Song'
import { Resolvers } from './schema.types'

Expand All @@ -21,7 +22,6 @@ export const graphqlRoot: Resolvers<Context> = {
Query: {
party: async (_, { partyName, partyPassword }) => {
const party = await Party.findOne({ name: partyName, password: partyPassword || null })
party?.sortVotedSongs()
return party || null
},
songs: () => Song.find(),
Expand All @@ -40,19 +40,39 @@ export const graphqlRoot: Resolvers<Context> = {
createParty: async (_, { partyName, partyPassword }) => {
const party = await new Party(partyName, partyPassword || undefined).save()
await party.reload() // We have to reload() because save() doesn't return the entire Party object.
party.sortVotedSongs()
return party
},
nextSong: async (_, { partyId }) => {
const party = await Party.findOne(partyId)
await party?.playNextSong()
await party?.reload()
party?.sortVotedSongs()
return party || null
},
},
// Rely on the resolver chain and async/partial resolution to perform the data conversion necessary for the API.
Party: {
latestTime: parent => parent.latestTime.toString(),
votedSongs: async self => {
return self.getSortedVotedSongs()
},
playedSongs: async self => {
// I cast here because playedSongs.song is a Promise, but I know that we'll just use the PlayedSong resolver to get an actual Song.
return (await self.playedSongs) as any
},
currentSong: async self => {
return await self.currentSong
},
},
VotedSong: {
song: async self => {
return self.getSong()
},
},
PlayedSong: {
song: async self => {
// For some reason the type of self is not PlayedSong so I have to cast it. TODO: investigate this.
const parentPlayedSong = (self as unknown) as PlayedSong
return parentPlayedSong.getSong()
},
},
}

0 comments on commit cb2aa64

Please sign in to comment.