refactor and cache bookmarks

This commit is contained in:
Thibault Duplessis 2012-06-09 01:17:37 +02:00
parent a56e459df8
commit b1ec3ca07e
36 changed files with 160 additions and 156 deletions

View file

@ -1,12 +1,12 @@
package lila
package star
package bookmark
import game.DbGame
import user.User
import org.joda.time.DateTime
case class Star(
case class Bookmark(
game: DbGame,
user: User,
date: DateTime) {

View file

@ -1,13 +1,14 @@
package lila
package star
package bookmark
import game.{ DbGame, GameRepo }
import user.{ User, UserRepo }
import scalaz.effects._
final class StarApi(
starRepo: StarRepo,
final class BookmarkApi(
bookmarkRepo: BookmarkRepo,
cached: Cached,
gameRepo: GameRepo,
userRepo: UserRepo,
paginator: PaginatorBuilder) {
@ -16,28 +17,29 @@ final class StarApi(
gameOption gameRepo game gameId
_ gameOption.fold(
game for {
bookmarked starRepo.toggle(game.id, user.id)
bookmarked bookmarkRepo.toggle(game.id, user.id)
_ gameRepo.incBookmarks(game.id, bookmarked.fold(1, -1))
_ io(cached invalidateUserId user.id)
} yield (),
io())
} yield ()
def starred(game: DbGame, user: User): IO[Boolean] =
starRepo.exists(game.id, user.id)
def bookmarked(game: DbGame, user: User): Boolean =
cached.bookmarked(game.id, user.id)
def countByUser(user: User): IO[Int] =
starRepo countByUserId user.id
def countByUser(user: User): Int =
cached.count(user.id)
def usersByGame(game: DbGame): IO[List[User]] = for {
userIds starRepo userIdsByGameId game.id
userIds bookmarkRepo userIdsByGameId game.id
users (userIds map userRepo.byId).sequence
} yield users.flatten
def removeByGame(game: DbGame): IO[Unit] =
starRepo removeByGameId game.id
bookmarkRepo removeByGameId game.id
def removeByGameIds(ids: List[String]): IO[Unit] =
starRepo removeByGameIds ids
bookmarkRepo removeByGameIds ids
def gamePaginatorByUser(user: User, page: Int) =
paginator.byUser(user: User, page: Int) map (_.game)

View file

@ -1,5 +1,5 @@
package lila
package star
package bookmark
import com.mongodb.casbah.MongoCollection
@ -7,7 +7,7 @@ import game.GameRepo
import user.UserRepo
import core.Settings
final class StarEnv(
final class BookmarkEnv(
settings: Settings,
gameRepo: GameRepo,
userRepo: UserRepo,
@ -15,16 +15,20 @@ final class StarEnv(
import settings._
lazy val starRepo = new StarRepo(mongodb(MongoCollectionStar))
lazy val bookmarkRepo = new BookmarkRepo(mongodb(MongoCollectionBookmark))
lazy val paginator = new PaginatorBuilder(
starRepo = starRepo,
bookmarkRepo = bookmarkRepo,
gameRepo = gameRepo,
userRepo = userRepo,
maxPerPage = GamePaginatorMaxPerPage)
lazy val api = new StarApi(
starRepo = starRepo,
lazy val cached = new Cached(
bookmarkRepo = bookmarkRepo)
lazy val api = new BookmarkApi(
bookmarkRepo = bookmarkRepo,
cached = cached,
gameRepo = gameRepo,
userRepo = userRepo,
paginator = paginator)

View file

@ -0,0 +1,16 @@
package lila
package bookmark
import core.CoreEnv
import game.DbGame
import user.User
trait BookmarkHelper {
protected def env: CoreEnv
private def api = env.bookmark.api
def bookmarked(game: DbGame, user: User): Boolean =
api.bookmarked(game, user)
}

View file

@ -1,5 +1,5 @@
package lila
package star
package bookmark
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
@ -8,12 +8,12 @@ import com.mongodb.MongoException.DuplicateKey
import scalaz.effects._
import org.joda.time.DateTime
// db.star.ensureIndex({g:1})
// db.star.ensureIndex({u:1})
// db.star.ensureIndex({d: -1})
final class StarRepo(val collection: MongoCollection) {
// db.bookmark.ensureIndex({g:1})
// db.bookmark.ensureIndex({u:1})
// db.bookmark.ensureIndex({d: -1})
final class BookmarkRepo(val collection: MongoCollection) {
private[star] def toggle(gameId: String, userId: String): IO[Boolean] = io {
private[bookmark] def toggle(gameId: String, userId: String): IO[Boolean] = io {
try {
add(gameId, userId, DateTime.now)
true
@ -26,18 +26,16 @@ final class StarRepo(val collection: MongoCollection) {
}
}
def exists(gameId: String, userId: String) = io {
(collection count idQuery(gameId, userId)) > 0
}
def countByUserId(userId: String) = io {
collection count userIdQuery(userId) toInt
}
def userIdsByGameId(gameId: String): IO[List[String]] = io {
(collection find gameIdQuery(gameId) sort sortQuery(1) map { obj
obj.getAs[String]("u")
}).toList.flatten
}).flatten.toList
}
def gameIdsByUserId(userId: String): IO[Set[String]] = io {
(collection find userIdQuery(userId) map { obj
obj.getAs[String]("g")
}).flatten.toSet
}
def removeByGameId(gameId: String): IO[Unit] = io {

25
app/bookmark/Cached.scala Normal file
View file

@ -0,0 +1,25 @@
package lila
package bookmark
import scala.collection.mutable
final class Cached(bookmarkRepo: BookmarkRepo) {
private val gameIdsCache = mutable.Map[String, Set[String]]()
def bookmarked(gameId: String, userId: String) =
userGameIds(userId)(gameId)
def count(userId: String) =
userGameIds(userId).size
def invalidateUserId(userId: String) = {
gameIdsCache -= userId
}
private def userGameIds(userId: String): Set[String] =
gameIdsCache.getOrElseUpdate(
userId,
(bookmarkRepo gameIdsByUserId userId).unsafePerformIO
)
}

View file

@ -1,5 +1,5 @@
package lila
package star
package bookmark
import game.{ DbGame, GameRepo }
import user.{ User, UserRepo }
@ -9,27 +9,27 @@ import com.mongodb.casbah.Imports._
import org.joda.time.DateTime
final class PaginatorBuilder(
starRepo: StarRepo,
bookmarkRepo: BookmarkRepo,
gameRepo: GameRepo,
userRepo: UserRepo,
maxPerPage: Int) {
def byUser(user: User, page: Int): Paginator[Star] =
def byUser(user: User, page: Int): Paginator[Bookmark] =
paginator(new UserAdapter(user), page)
private def paginator(adapter: Adapter[Star], page: Int): Paginator[Star] =
private def paginator(adapter: Adapter[Bookmark], page: Int): Paginator[Bookmark] =
Paginator(
adapter,
currentPage = page,
maxPerPage = maxPerPage
).fold(_ paginator(adapter, 0), identity)
final class UserAdapter(user: User) extends Adapter[Star] {
final class UserAdapter(user: User) extends Adapter[Bookmark] {
def nbResults: Int = starRepo.collection count query toInt
def nbResults: Int = bookmarkRepo.collection count query toInt
def slice(offset: Int, length: Int): Seq[Star] = {
val objs = ((starRepo.collection find query sort sort skip offset limit length).toList map { obj
def slice(offset: Int, length: Int): Seq[Bookmark] = {
val objs = ((bookmarkRepo.collection find query sort sort skip offset limit length).toList map { obj
for {
gameId obj.getAs[String]("g")
date obj.getAs[DateTime]("d")
@ -38,12 +38,12 @@ final class PaginatorBuilder(
val games = (gameRepo games objs.map(_._1)).unsafePerformIO
objs map { obj
games find (_.id == obj._1) map { game
Star(game, user, obj._2)
Bookmark(game, user, obj._2)
}
}
} flatten
private def query = starRepo userIdQuery user.id
private def sort = starRepo sortQuery -1
private def query = bookmarkRepo userIdQuery user.id
private def sort = bookmarkRepo sortQuery -1
}
}

View file

@ -13,11 +13,11 @@ object Analyse extends LilaController {
def gameRepo = env.game.gameRepo
def pgnDump = env.analyse.pgnDump
def openingExplorer = chess.OpeningExplorer
def starApi = env.star.api
def bookmarkApi = env.bookmark.api
def replay(id: String, color: String) = Open { implicit ctx
IOptionIOk(gameRepo.pov(id, color)) { pov
starApi usersByGame pov.game map { bookmarkers
bookmarkApi usersByGame pov.game map { bookmarkers
html.analyse.replay(
pov,
bookmarkers,

View file

@ -11,16 +11,15 @@ object Game extends LilaController {
val gameRepo = env.game.gameRepo
val paginator = env.game.paginator
val cached = env.game.cached
val starApi = env.star.api
val bookmarkApi = env.bookmark.api
val listMenu = env.game.listMenu
val maxPage = 40
val realtime = Open { implicit ctx
IOk(for {
games gameRepo recentGames 9
menu makeListMenu
} yield html.game.realtime(games, menu))
IOk(gameRepo recentGames 9 map { games
html.game.realtime(games, makeListMenu)
})
}
def realtimeInner(ids: String) = Open { implicit ctx
@ -31,34 +30,26 @@ object Game extends LilaController {
def all(page: Int) = Open { implicit ctx
reasonable(page) {
IOk(makeListMenu map { menu
html.game.all(paginator recent page, menu)
})
Ok(html.game.all(paginator recent page, makeListMenu))
}
}
def checkmate(page: Int) = Open { implicit ctx
reasonable(page) {
IOk(makeListMenu map { menu
html.game.checkmate(paginator checkmate page, menu)
})
Ok(html.game.checkmate(paginator checkmate page, makeListMenu))
}
}
def star(page: Int) = Auth { implicit ctx
def bookmark(page: Int) = Auth { implicit ctx
me
reasonable(page) {
IOk(makeListMenu map { menu
html.game.star(starApi.gamePaginatorByUser(me, page), menu)
})
Ok(html.game.bookmarked(bookmarkApi.gamePaginatorByUser(me, page), makeListMenu))
}
}
def popular(page: Int) = Open { implicit ctx
reasonable(page) {
IOk(makeListMenu map { menu
html.game.popular(paginator popular page, menu)
})
Ok(html.game.popular(paginator popular page, makeListMenu))
}
}
@ -66,5 +57,5 @@ object Game extends LilaController {
(page < maxPage).fold(result, BadRequest("too old"))
private def makeListMenu(implicit ctx: Context) =
listMenu(starApi.countByUser, ctx.me)
listMenu(bookmarkApi.countByUser, ctx.me)
}

View file

@ -21,7 +21,7 @@ object Round extends LilaController {
def messenger = env.round.messenger
def rematcher = env.setup.rematcher
def joiner = env.setup.friendJoiner
def starApi = env.star.api
def bookmarkApi = env.bookmark.api
def websocketWatcher(gameId: String, color: String) = WebSocket.async[JsValue] { req
implicit val ctx = reqToCtx(req)
@ -59,7 +59,7 @@ object Round extends LilaController {
private def watch(pov: Pov)(implicit ctx: Context): IO[Result] =
pov.game.hasBookmarks.fold(
starApi usersByGame pov.game,
bookmarkApi usersByGame pov.game,
io(Nil)
) map { bookmarkers
Ok(html.round.watcher(pov, version(pov.gameId), bookmarkers))

View file

@ -16,7 +16,7 @@ object Setup extends LilaController {
def processor = env.setup.processor
def friendConfigMemo = env.setup.friendConfigMemo
def gameRepo = env.game.gameRepo
def starApi = env.star.api
def bookmarkApi = env.bookmark.api
val aiForm = Open { implicit ctx
IOk(forms.aiFilled map { html.setup.ai(_) })
@ -69,7 +69,7 @@ object Setup extends LilaController {
io(routes.Round.player(pov.fullId)),
for {
_ gameRepo remove pov.game.id
_ starApi removeByGame pov.game
_ bookmarkApi removeByGame pov.game
} yield routes.Lobby.home
)
}

View file

@ -6,9 +6,9 @@ import views._
import play.api.mvc._
import scalaz.effects._
object Star extends LilaController {
object Bookmark extends LilaController {
val api = env.star.api
val api = env.bookmark.api
val gameRepo = env.game.gameRepo
def toggle(gameId: String) = Auth { implicit ctx

View file

@ -19,18 +19,18 @@ object User extends LilaController {
def forms = user.DataForm
def eloUpdater = env.user.eloUpdater
def lobbyMessenger = env.lobby.messenger
def starApi = env.star.api
def bookmarkApi = env.bookmark.api
def show(username: String) = showFilter(username, "all", 1)
def showFilter(username: String, filterName: String, page: Int) = Open { implicit ctx
IOptionIOk(userRepo byId username) { u
u.enabled.fold(
env.user.userInfo(u, starApi, ctx) map { info
env.user.userInfo(u, bookmarkApi, ctx) map { info
val filters = user.GameFilterMenu(info, ctx.me, filterName)
val paginator = filters.query.fold(
query gamePaginator.recentlyCreated(query)(page),
starApi.gamePaginatorByUser(u, page))
bookmarkApi.gamePaginatorByUser(u, page))
html.user.show(u, info, paginator, filters)
},
io(html.user.disabled(u)))

View file

@ -92,7 +92,7 @@ final class CoreEnv private (application: Application, val settings: Settings) {
gameRepo = game.gameRepo,
userRepo = user.userRepo)
lazy val star = new lila.star.StarEnv(
lazy val bookmark = new lila.bookmark.BookmarkEnv(
settings = settings,
gameRepo = game.gameRepo,
userRepo = user.userRepo,
@ -120,7 +120,7 @@ final class CoreEnv private (application: Application, val settings: Settings) {
lazy val titivate = new core.Titivate(
gameRepo = game.gameRepo,
finisher = round.finisher,
starApi = star.api)
bookmarkApi = bookmark.api)
}
object CoreEnv {

View file

@ -73,7 +73,7 @@ final class Settings(config: Config) {
val MongoCollectionMessageThread = getString("mongo.collection.message_thread")
val MongoCollectionWikiPage = getString("mongo.collection.wiki_page")
val MongoCollectionFirewall = getString("mongo.collection.firewall")
val MongoCollectionStar = getString("mongo.collection.star")
val MongoCollectionBookmark = getString("mongo.collection.bookmark")
val FirewallCacheTtl = millis("firewall.cache_ttl")

View file

@ -3,7 +3,7 @@ package core
import game.GameRepo
import round.Finisher
import star.StarApi
import bookmark.BookmarkApi
import com.mongodb.casbah.Imports._
import org.joda.time.DateTime
@ -13,7 +13,7 @@ import scalaz.effects._
final class Titivate(
gameRepo: GameRepo,
finisher: Finisher,
starApi: StarApi) {
bookmarkApi: BookmarkApi) {
val finishByClock: IO[Unit] =
for {
@ -24,7 +24,7 @@ final class Titivate(
val cleanupUnplayed = for {
ids gameRepo.unplayedIds
_ gameRepo removeIds ids
_ starApi removeByGameIds ids
_ bookmarkApi removeByGameIds ids
} yield ()
val cleanupNext: IO[Unit] = {

View file

@ -1,17 +1,7 @@
package lila
package game
import akka.actor._
import akka.pattern.ask
import akka.util.duration._
import akka.util.Timeout
import akka.dispatch.{ Future, Await }
import akka.pattern.ask
import akka.util.duration._
import akka.util.Timeout
import play.api.Play.current
import play.api.libs.concurrent._
import memo.ActorMemo

View file

@ -47,7 +47,7 @@ trait GameHelper { self: I18nHelper with UserHelper with StringHelper ⇒
isUsernameOnline(username).fold(" online", " offline"),
""),
routes.User.show(username),
usernameWithElo(player) + player.eloDiff.filter(_ withDiff.pp).fold(
usernameWithElo(player) + player.eloDiff.filter(_ withDiff).fold(
diff " (%s)".format(showNumber(diff)),
"")
)

View file

@ -140,7 +140,7 @@ class GameRepo(collection: MongoCollection)
)
}
// stars should also be removed
// bookmarks should also be removed
def remove(id: String): IO[Unit] = io {
remove(idSelector(id))
}

View file

@ -9,21 +9,16 @@ case class ListMenu(
nbGames: Int,
nbMates: Int,
nbPopular: Int,
nbStars: Option[Int])
nbBookmarks: Option[Int])
object ListMenu {
type CountStars = User IO[Int]
type CountBookmarks = User Int
def apply(cached: Cached)(countStars: CountStars, me: Option[User]): IO[ListMenu] =
me.fold(
m countStars(m) map (_.some),
io(none)
) map { nbStars
new ListMenu(
nbGames = cached.nbGames,
nbMates = cached.nbMates,
nbPopular = cached.nbPopular,
nbStars = nbStars)
}
def apply(cached: Cached)(countBookmarks: CountBookmarks, me: Option[User]): ListMenu =
new ListMenu(
nbGames = cached.nbGames,
nbMates = cached.nbMates,
nbPopular = cached.nbPopular,
nbBookmarks = me map countBookmarks)
}

View file

@ -1 +0,0 @@

View file

@ -1,16 +0,0 @@
package lila
package star
import core.CoreEnv
import game.DbGame
import user.User
trait StarHelper {
protected def env: CoreEnv
private def api = env.star.api
def starred(game: DbGame, user: User): Boolean =
api.starred(game, user).unsafePerformIO
}

View file

@ -27,7 +27,7 @@ object Environment
with forum.ForumHelper
with security.SecurityHelper
with i18n.I18nHelper
with star.StarHelper {
with bookmark.BookmarkHelper {
protected def env = coreEnv
}

View file

@ -6,7 +6,7 @@ import scala.collection.mutable
final class Cached(userRepo: UserRepo) {
// id => username
val usernameCache = mutable.Map[String, Option[String]]()
private val usernameCache = mutable.Map[String, Option[String]]()
def username(userId: String) =
usernameCache.getOrElseUpdate(

View file

@ -4,7 +4,7 @@ package user
import chess.{ EloCalculator, Color }
import game.{ GameRepo, DbGame }
import http.Context
import star.StarApi
import bookmark.BookmarkApi
import scalaz.effects._
@ -34,7 +34,7 @@ object UserInfo {
eloCalculator: EloCalculator,
eloChartBuilder: User IO[Option[EloChart]])(
user: User,
starApi: StarApi,
bookmarkApi: BookmarkApi,
ctx: Context): IO[UserInfo] = for {
rank (user.elo >= 1500).fold(
userRepo rank user flatMap { rank
@ -54,7 +54,7 @@ object UserInfo {
me gameRepo count (_.opponents(user, me)) map (_.some),
io(none)
)
nbBookmark starApi countByUser user
nbBookmark = bookmarkApi countByUser user
eloChart eloChartBuilder(user)
winChart = (user.nbRatedGames > 0) option {
new WinChart(nbWin, nbDraw, nbLoss)

View file

@ -1,11 +1,11 @@
@(g: DbGame)(implicit ctx: Context)
<span class="star_game">
<span class="bookmark">
@ctx.me.map { m =>
<a
href="@routes.Star.toggle(g.id)"
href="@routes.Bookmark.toggle(g.id)"
class="icon
@starred(g, m).fold("starred", "")"
@bookmarked(g, m).fold("bookmarked", "")"
title="@trans.bookmarkThisGame()">@g.showBookmarks</a>
}.getOrElse {
<span class="icon">@g.showBookmarks</span>

View file

@ -0,0 +1,7 @@
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.list(
name = trans.nbBookmarks.str(listMenu.nbBookmarks.fold(_.localize, 0)),
paginator = paginator,
next = paginator.nextPage map { n => routes.Game.bookmark(n) },
menu = sideMenu(listMenu, "bookmark"))

View file

@ -9,8 +9,8 @@
<a class="@active.active("checkmate")" href="@routes.Game.checkmate()">
@trans.viewNbCheckmates(listMenu.nbMates.localize)
</a>
@listMenu.nbStars.map { nb =>
<a class="@active.active("star")" href="@routes.Game.star()">
@listMenu.nbBookmarks.map { nb =>
<a class="@active.active("bookmark")" href="@routes.Game.bookmark()">
@trans.nbBookmarks(nb.localize)
</a>
<a class="@active.active("popular")" href="@routes.Game.popular()">

View file

@ -1,7 +0,0 @@
@(paginator: Paginator[DbGame], listMenu: lila.game.ListMenu)(implicit ctx: Context)
@game.list(
name = trans.nbBookmarks.str(listMenu.nbStars.fold(_.localize, 0)),
paginator = paginator,
next = paginator.nextPage map { n => routes.Game.star(n) },
menu = sideMenu(listMenu, "star"))

View file

@ -6,7 +6,7 @@
@defining(fromPlayer | g.creator ) { firstPlayer =>
@gameFen(g, firstPlayer.color.some)
<div class="infos">
@game.bookmark(g)
@bookmark.toggle(g)
<b>
@if(g.isBeingPlayed) {
<a class="link" href="@routes.Round.watcher(g.id, firstPlayer.color.name)">@trans.playingRightNow()</a>,

View file

@ -2,7 +2,7 @@
@defining("http://lichess.org" + routes.Round.watcher(g.id, "white")) { url =>
<div class="game_share clearfix">
@game.bookmark(g)
@bookmark.toggle(g)
<a
title="@trans.shareThisUrlToLetSpectatorsSeeTheGame()"
class="game_permalink blank_if_play"

View file

@ -19,7 +19,7 @@ mongo {
message_thread = m_thread
wiki_page = wiki
firewall = firewall
star = star
bookmark = bookmark
}
connectionsPerHost = 100
autoConnectRetry = true

View file

@ -6,7 +6,7 @@ GET /games controllers.Game.realtime
GET /games/refresh/:ids controllers.Game.realtimeInner(ids: String)
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
GET /games/bookmark controllers.Game.star(page: Int ?= 1)
GET /games/bookmark controllers.Game.bookmark(page: Int ?= 1)
GET /games/popular controllers.Game.popular(page: Int ?= 1)
# Round
@ -94,8 +94,8 @@ GET /new/$hookId<[\w\-]{8}>/join controllers.Lobby.join(hookId: String)
GET /lobby/socket controllers.Lobby.socket
GET /lobby/log controllers.Lobby.log
# Star
POST /star/$gameId<[\w\-]{8}> controllers.Star.toggle(gameId: String)
# Bookmark
POST /bookmark/$gameId<[\w\-]{8}> controllers.Bookmark.toggle(gameId: String)
# Forum
GET /forum controllers.ForumCateg.index

View file

@ -162,20 +162,20 @@ $(function() {
return confirm('Confirm this action?');
});
function stars() {
$('span.star_game a.icon:not(.jsed)').each(function() {
function bookmarks() {
$('span.bookmark a.icon:not(.jsed)').each(function() {
var t = $(this).addClass("jsed");
t.click(function() {
t.toggleClass("starred");
t.toggleClass("bookmarked");
$.post(t.attr("href"));
var count = (parseInt(t.html()) || 0) + (t.hasClass("starred") ? 1 : -1);
var count = (parseInt(t.html()) || 0) + (t.hasClass("bookmarked") ? 1 : -1);
t.html(count > 0 ? count : "");
return false;
});
});
}
stars();
$('body').on('lichess.content_loaded', stars);
bookmarks();
$('body').on('lichess.content_loaded', bookmarks);
var elem = document.createElement('audio');
var canPlayAudio = !! elem.canPlayType && elem.canPlayType('audio/ogg; codecs="vorbis"');

View file

@ -590,12 +590,12 @@ div.game_share a.game_permalink {
margin-top: 3px;
}
span.star_game {
span.bookmark {
position: relative;
float: right;
margin-right: 40px;
}
span.star_game .icon {
span.bookmark .icon {
position: absolute;
top: 0;
left: 0;
@ -607,6 +607,6 @@ span.star_game .icon {
text-decoration: none;
color: #a0a0a0;
}
span.star_game .icon.starred {
span.bookmark .icon.bookmarked {
background-position: 0 -24px;
}

2
todo
View file

@ -28,7 +28,7 @@ remove bookmark when removing game
fix game list translations
next deploy:
db.game2.ensureIndex({"bm": -1}, {sparse: true})
db.star.renameCollection("bookmark")
new translations:
-rematchOfferCanceled=Rematch offer canceled