Showing preview only (231K chars total). Download the full file or copy to clipboard to get everything.
Repository: brikis98/ping-play
Branch: master
Commit: eae04fe4fd18
Files: 99
Total size: 200.8 KB
Directory structure:
gitextract_o11b8m2f/
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── big-pipe/
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── ybrikman/
│ │ │ └── ping/
│ │ │ └── javaapi/
│ │ │ ├── bigpipe/
│ │ │ │ ├── BigPipe.java
│ │ │ │ ├── HtmlPagelet.java
│ │ │ │ ├── HtmlStreamHelper.java
│ │ │ │ ├── JsonPagelet.java
│ │ │ │ ├── Pagelet.java
│ │ │ │ ├── PageletContentType.java
│ │ │ │ ├── PageletRenderOptions.java
│ │ │ │ └── TextPagelet.java
│ │ │ ├── dedupe/
│ │ │ │ ├── CacheFilter.java
│ │ │ │ └── DedupingCache.java
│ │ │ └── promise/
│ │ │ ├── Function2.java
│ │ │ ├── Function3.java
│ │ │ ├── Function4.java
│ │ │ ├── Function5.java
│ │ │ ├── Function6.java
│ │ │ ├── Promise2.java
│ │ │ ├── Promise3.java
│ │ │ ├── Promise4.java
│ │ │ ├── Promise5.java
│ │ │ ├── Promise6.java
│ │ │ └── PromiseHelper.java
│ │ ├── resources/
│ │ │ └── public/
│ │ │ └── com/
│ │ │ └── ybrikman/
│ │ │ └── ping/
│ │ │ └── big-pipe.js
│ │ ├── scala/
│ │ │ └── com/
│ │ │ └── ybrikman/
│ │ │ └── ping/
│ │ │ └── scalaapi/
│ │ │ ├── bigpipe/
│ │ │ │ ├── BigPipe.scala
│ │ │ │ ├── Embed.scala
│ │ │ │ ├── HtmlStream.scala
│ │ │ │ ├── JavaAdapter.scala
│ │ │ │ └── Pagelet.scala
│ │ │ ├── compose/
│ │ │ │ └── Compose.scala
│ │ │ └── dedupe/
│ │ │ ├── BeforeAndAfterFilter.scala
│ │ │ ├── Cache.scala
│ │ │ ├── CacheFilter.scala
│ │ │ ├── CacheNotInitializedException.scala
│ │ │ └── DedupingCache.scala
│ │ └── twirl/
│ │ └── com/
│ │ └── ybrikman/
│ │ └── bigpipe/
│ │ ├── css.scala.html
│ │ ├── js.scala.html
│ │ ├── pageletClientSide.scala.html
│ │ └── pageletServerSide.scala.html
│ └── test/
│ └── scala/
│ └── com/
│ └── ybrikman/
│ └── ping/
│ ├── javaapi/
│ │ └── dedupe/
│ │ ├── TestCacheFilter.scala
│ │ └── TestDedupingCache.scala
│ └── scalaapi/
│ ├── bigpipe/
│ │ ├── TestBigPipeJavaScript.scala
│ │ └── TestEmbed.scala
│ └── dedupe/
│ ├── TestBeforeAndAfterFilter.scala
│ ├── TestCache.scala
│ ├── TestCacheFilter.scala
│ └── TestDedupingCache.scala
├── build.sbt
├── circle.yml
├── docker-compose.yml
├── project/
│ ├── build.properties
│ └── plugins.sbt
├── sample-app-common/
│ └── src/
│ ├── main/
│ │ ├── resources/
│ │ │ └── public/
│ │ │ ├── javascripts/
│ │ │ │ ├── big-pipe-with-mustache.js
│ │ │ │ ├── mustache.js
│ │ │ │ └── timing.js
│ │ │ └── stylesheets/
│ │ │ └── main.css
│ │ ├── scala/
│ │ │ └── data/
│ │ │ ├── FakeServiceClient.scala
│ │ │ ├── FutureUtil.scala
│ │ │ ├── Response.scala
│ │ │ └── UrlAndId.scala
│ │ └── twirl/
│ │ └── views/
│ │ ├── clientSideTemplating.scala.stream
│ │ ├── dedupe.scala.html
│ │ ├── escaping.scala.stream
│ │ ├── helpers/
│ │ │ ├── error.scala.html
│ │ │ ├── module.scala.html
│ │ │ └── timing.scala.html
│ │ ├── withBigPipe.scala.stream
│ │ └── withoutBigPipe.scala.html
│ └── test/
│ └── scala/
│ └── com/
│ └── ybrikman/
│ └── ping/
│ ├── BaseBigPipeSpec.scala
│ ├── BaseBigPipeTimingSpec.scala
│ ├── BaseDedupeSpec.scala
│ └── PingSpecification.scala
├── sample-app-java/
│ ├── app/
│ │ ├── controllers/
│ │ │ ├── Deduping.java
│ │ │ ├── Mock.java
│ │ │ ├── MoreBigPipeExamples.java
│ │ │ ├── WithBigPipe.java
│ │ │ └── WithoutBigPipe.java
│ │ ├── data/
│ │ │ └── ServiceClient.java
│ │ ├── helper/
│ │ │ └── FakeServiceClient.java
│ │ └── loader/
│ │ └── Filters.java
│ ├── conf/
│ │ ├── application.conf
│ │ └── routes
│ └── test/
│ └── com/
│ └── ybrikman/
│ └── ping/
│ ├── PingJavaTestComponents.scala
│ └── Tests.scala
├── sample-app-scala/
│ ├── app/
│ │ ├── controllers/
│ │ │ ├── Deduping.scala
│ │ │ ├── Mock.scala
│ │ │ ├── MoreBigPipeExamples.scala
│ │ │ ├── WithBigPipe.scala
│ │ │ └── WithoutBigPipe.scala
│ │ ├── data/
│ │ │ └── ServiceClient.scala
│ │ └── loader/
│ │ └── PingApplicationLoader.scala
│ ├── conf/
│ │ ├── application.conf
│ │ └── routes
│ └── test/
│ └── com/
│ └── ybrikman/
│ └── ping/
│ ├── PingScalaTestComponents.scala
│ └── Tests.scala
└── version.sbt
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
config
.project
.settings
.classpath
.cache
*.iml
*.iws
*.ipr
.idea/
build/
*/build/
out/
*/bin/
bin
codegen/
*Generated*/
test-output/
*/.target
target
.target
*/target
logs
.cache
dist
project/project
project/target
tmp
.history
.idea_modules
RUNNING_PID
.svn
.DS_Store
.git
================================================
FILE: .gitignore
================================================
config
.project
.settings
.classpath
.cache
*.iml
*.iws
*.ipr
.idea/
build/
*/build/
out/
*/bin/
bin
codegen/
*Generated*/
test-output/
*/.target
target
.target
*/target
logs
.cache
dist
project/project
project/target
tmp
.history
.idea_modules
RUNNING_PID
.svn
.DS_Store
.vagrant
================================================
FILE: Dockerfile
================================================
# Based off an image that has JDK8 on busybox
FROM frolvlad/alpine-oraclejdk8:cleaned
MAINTAINER Yevgeniy Brikman <jim@ybrikman.com>
RUN apk --update add bash
# Set up activator
RUN wget http://downloads.typesafe.com/typesafe-activator/1.3.2/typesafe-activator-1.3.2-minimal.zip \
&& unzip typesafe-activator-1.3.2-minimal.zip \
&& rm -f typesafe-activator-1.3.2-minimal.zip \
&& chmod +x activator-1.3.2-minimal/activator
ENV PATH $PATH:/activator-1.3.2-minimal
# The source code will be in the /src folder
RUN mkdir -p /src
VOLUME /src
WORKDIR /src
COPY . /src
# Use a global SBT config to setup an external target directory so that the
# compiled code isn't blown away if the user mounts a src folder from their
# host OS.
RUN mkdir -p /sbt-target \
&& mkdir -p ~/.sbt/0.13/ \
&& echo 'target := file("/sbt-target") / s"${name.value}-target"' > ~/.sbt/0.13/global.sbt
# Build the entire app so that all the dependencies are downloaded and all the
# code is compiled. This will make starting the app the first time much faster.
RUN activator dist
# Expose play port
EXPOSE 9000
# Default command is to run the app
CMD ["activator", "run"]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2014 Yevgeniy Brikman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Ping-Play
The ping-play project brings [BigPipe](https://www.facebook.com/note.php?note_id=389414033919) streaming to the
[Play Framework](http://playframework.com/). It includes tools for a) splitting your pages up into small "pagelets",
which makes it easier to maintain large websites, and b) streaming those pagelets down to the browser as soon as they
are ready, which can significantly reduce page load time.
To fetch the data for a page, modern apps often have to make requests to multiple remote backend services (e.g. RESTful
HTTP calls to a profile service, a search service, an ads service, etc). You then have to wait for *all* of these
remote calls to come back before you can send *any* data back to the browser. For example, the following screen capture
shows a page that makes 6 remote service calls, most of which complete in few hundred milliseconds, but one takes
over 5 seconds. As a result, the time to first byte is 5 seconds, during which the user sees a completely blank page:

With BigPipe, you can start streaming data back to the browser without waiting for the backends at all, and fill in the
page incrementally as each backend responds. For example, the following screen capture shows the same page making the
same 6 remote service calls, but this time rendered using BigPipe. The header and much of the markup is sent back
instantly, so time to first byte is 10 milliseconds (instead of 5 seconds), static content (i.e. CSS, JS, images) can
start loading right away, and then, as each backend service responds, the corresponding part of the page (i.e. the
pagelet) is sent to the browser and rendered on the screen:

# Quick start
To understand how to transform your Play app to use BigPipe, it's helpful to first see an example that does *not* use
BigPipe (note, the example is in Scala, but ping-play supports Java too!). Here is the controller code,
[controllers/WithoutBigPipe.scala](sample-app-scala/app/controllers/WithoutBigPipe.scala), for the example mentioned
earlier:
```scala
class WithoutBigPipe(serviceClient: FakeServiceClient) extends Controller {
def index = Action.async { implicit request =>
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallFast("ads")
val searchFuture = serviceClient.fakeRemoteCallFast("search")
// Wait for all the remote calls to complete
for {
profile <- profileFuture
graph <- graphFuture
feed <- feedFuture
inbox <- inboxFuture
ads <- adsFuture
search <- searchFuture
} yield {
// Render the template once all the data is available
Ok(views.html.withoutBigPipe(profile, graph, feed, inbox, ads, search))
}
}
}
```
This controller makes 6 remote service calls, gets back 6 `Future` objects, and when they have all redeemed, it uses
them to render the following template, [views/withoutBigPipe.scala.html](sample-app-common/src/main/twirl/views/withoutBigPipe.scala.html):
```html
@(profile: data.Response, graph: data.Response, feed: data.Response, inbox: data.Response, ads: data.Response, search: data.Response)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
</head>
<body>
<h1>Without Big Pipe</h1>
<table class="wrapper">
<tr>
<td><div id="profile">@views.html.helpers.module(profile)</div></td>
<td><div id="ads">@views.html.helpers.module(ads)</div></td>
<td><div id="feed">@views.html.helpers.module(feed)</div></td>
</tr>
<tr>
<td><div id="search">@views.html.helpers.module(search)</div></td>
<td><div id="inbox">@views.html.helpers.module(inbox)</div></td>
<td><div id="graph">@views.html.helpers.module(graph)</div></td>
</tr>
</table>
</body>
</html>
```
When you load this page, nothing will show up on the screen until all of the backend calls complete, which will take
about 5 seconds.
To transform this page to use BigPipe, you first add the big-pipe dependency to your build (note, this project requires
Play 2.4, Scala 2.11.6, SBT 0.13.8, and Java 8):
```scala
libraryDependencies += "com.ybrikman.ping" %% "big-pipe" % "0.0.13"
```
Next, add support for the `.scala.stream` template type and some imports for it to your build:
```scala
TwirlKeys.templateFormats ++= Map("stream" -> "com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamFormat"),
TwirlKeys.templateImports ++= Vector("com.ybrikman.ping.scalaapi.bigpipe.HtmlStream", "com.ybrikman.ping.scalaapi.bigpipe._")
```
Now you can create streaming templates. These templates can mix normal HTML markup, which will be streamed to the
browser immediately, with the `HtmlStream` class, which is a wrapper for an `Enumerator[Html]` that will be streamed
to the browser whenever the `Enumerator` has data. Here is [views/withBigPipe.scala.stream](sample-app-common/src/main/twirl/views/withBigPipe.scala.stream),
which is the streaming version of the template above:
```html
@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
<!-- You need to include the BigPipe JavaScript at the top of the page -->
<script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
</head>
<body>
<h1>With Big Pipe</h1>
@HtmlStream.fromHtml(views.html.helpers.timing())
<!--
Wrap the entire body of your page with a bigPipe.render call. The pagelets parameter contains a Map from
Pagelet id to the HtmlStream for that Pagelet. You should put the HtmlStream for each of your Pagelets
into the appropriate place in the markup.
-->
@bigPipe.render { pagelets =>
<table class="wrapper">
<tr>
<td>@pagelets(profile.id)</td>
<td>@pagelets(ads.id)</td>
<td>@pagelets(feed.id)</td>
</tr>
<tr>
<td>@pagelets(search.id)</td>
<td>@pagelets(inbox.id)</td>
<td>@pagelets(graph.id)</td>
</tr>
</table>
}
</body>
</html>
```
The key changes to notice from the original template are:
1. Most of the markup in the page is wrapped in a call to the `BigPipe.render` method.
2. The `BigPipe.render` method gives you a parameter, named `pagelets` in the example above, that is a `Map`
from Pagelet `id` to the `HtmlStream` for that Pagelet. The idea is to place the `HtmlStream` for each of your
Pagelets into the proper place in the markup where that Pagelet should appear.
3. You need to include `big-pipe.js` in the `head` of the document.
Now, let's look at the controller you can use with this template, called [controllers/WithBigPipe.scala](sample-app-scala/app/controllers/WithBigPipe.scala):
```scala
class WithBigPipe(serviceClient: FakeServiceClient) extends Controller {
def index = Action {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallFast("ads")
val searchFuture = serviceClient.fakeRemoteCallFast("search")
// Convert each Future into a Pagelet which will be rendered as HTML as soon as the data is available
val profile = HtmlPagelet("profile", profileFuture.map(views.html.helpers.module.apply))
val graph = HtmlPagelet("graph", graphFuture.map(views.html.helpers.module.apply))
val feed = HtmlPagelet("feed", feedFuture.map(views.html.helpers.module.apply))
val inbox = HtmlPagelet("inbox", inboxFuture.map(views.html.helpers.module.apply))
val ads = HtmlPagelet("ads", adsFuture.map(views.html.helpers.module.apply))
val search = HtmlPagelet("search", searchFuture.map(views.html.helpers.module.apply))
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
val bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search)
Ok.chunked(views.stream.withBigPipe(bigPipe, profile, graph, feed, inbox, ads, search))
}
}
```
The key changes to notice from the original controller are:
1. Instead of waiting for *all* of the service calls to redeem, you render each one individually into `Html` as soon as
the data is available, giving you a `Future[Html]`.
2. Each `Future[Html]`, plus the DOM id of where in the DOM it should be inserted, is wrapped in an `HtmlPagelet`
object.
3. The `HtmlPagelet` objects are composed into a `BigPipe` object, and told to use client-side rendering.
4. This `BigPipe` instance and all the `HtmlPagelet` objects are passed to the streaming template for rendering.
When you load this page, you will see the outline of the page almost immediately, and each piece of the page will
fill in this outline as soon as the corresponding remote service responds.
# More examples
There are several BigPipe examples, including the one described above, in [sample-app-scala](sample-app-scala) and
[sample-app-java](sample-app-java) in this repo (yes, BigPipe streaming works with both Scala and Java). You'll also
want to browse [sample-app-common](sample-app-common), which has some code shared by both sample apps, including all of
their templates. For example, here is how to run the Scala sample app (assuming you have
[Typesafe Activator](https://www.typesafe.com/community/core-tools/activator-and-sbt) installed already):
1. `git clone` this repo.
2. `activator shell`
3. `project sampleAppScala`
4. `run`
5. Open `http://localhost:9000/withoutBigPipe` to see how long the page takes to load without BigPipe streaming.
6. Open `http://localhost:9000/withBigPipe` to see how much faster the page loads with BigPipe streaming.
Check out the [Documentation](#Documentation) to see what APIs are available and [FAQ](#FAQ) to learn more about
BigPipe.
# Documentation
## Scala vs Java
BigPipe streaming is supported for both Scala and Java developers.
Scala developers should primarily be using classes in the `com.ybrikman.ping.scalaapi` package. In particular, use the
`com.ybrikman.ping.scalaapi.bigpipe.HtmlPagelet` class to wrap your `Future[Html]` objects as `Pagelet` objects, and
use the `com.ybrikman.ping.scalaapi.bigpipe.BigPipe` class to compose and render your `Pagelet` objects. See
[sample-app-scala](sample-app-scala) for examples.
Java developers should primarily be using classes in the `com.ybrikman.ping.javaapi` package. In particular, use the
`com.ybrikman.ping.javaapi.bigpipe.HtmlPagelet` class to wrap your and `Promise<Html>` as `Pagelet` objects and use the
`com.ybrikman.ping.javaapi.bigpipe.BigPipe` class to compose and render your `Pagelet` objects. See
[sample-app-java](sample-app-java) for examples.
## Client-side vs server-side rendering
Ping-Play supports both client-side and server-side BigPipe streaming. Client-side streaming sends down the
pagelets in whatever order they complete and uses JavaScript to insert each pagelet into the correct spot in the DOM.
This gives you the fastest possible loading time, but it does add a dependency on JavaScript. For use cases where you
want to avoid JavaScript, such as slower browsers or search engine crawlers (i.e. SEO), you can use server-side
rendering, which sends all the pagelets down already rendered as HTML and in the proper order. This will have a longer
page-load time than client-side rendering, but still much faster than not using BigPipe at all.
The *only* part of your code that you have to change to switch between server-side and client-side rendering is the
`PageletRenderOptions` parameter you pass into the `BigPipe` constructor. Here is an example of how you could check
the `User-Agent` header and select `PageletRenderOptions.ServerSide` if you detect GoogleBot and
`PageletRenderOptions.ClientSide` otherwise:
```scala
def index = Action { request =>
// ... fetch data, create pagelets ...
val bigPipe = new BigPipe(renderOptions(request), pagelet1, pagelet2, ...)
// ... render a streaming template ...
}
private def renderOptions(request: RequestHeader): PageletRenderOptions = {
request.headers.get(HeaderNames.USER_AGENT) match {
case Some(header) if header.contains("GoogleBot") => PageletRenderOptions.ServerSide
case _ => PageletRenderOptions.ClientSide
}
}
```
## HtmlStream and .scala.stream templates
Play's built-in `.scala.html` templates are compiled into functions that append together and return `Html`, which is
just a wrapper for a `StringBuilder`, and cannot be streamed. This is why this project introduces a new `.scala.stream`
template that appends together and returns `HtmlStream` objects, which are a wrapper for an `Enumerator[Html]`, which
can be streamed. Note that this new template type still uses Play's [Twirl](https://github.com/playframework/twirl)
template compiler and its syntax. The only things that are different are:
1. The extension is `.scala.stream` instead of `.scala.html`.
2. When you are using the template in a controller, the package name will be `views.stream.XXX` instead of
`views.html.XXX`.
3. To include raw, unescaped HTML, instead of wrapping the content in an `Html` object (e.g.
`Html(someStringWithMarkup)`), wrap it in an `HtmlStream` object (e.g. `HtmlStream.fromString(someStringWithMarkup)`).
4. You can include an `HtmlStream` object anywhere in the markup of a `.scala.stream` template and Play will stream the
content down from the `HtmlStream`'s `Enumerator` whenever the content is available.
The last point is how you get BigPipe style streaming. The `HtmlStream` class has many helper methods to create an
`HtmlStream`, including `fromHtml` and `fromHtmlFuture`, and to compose several streams into one, such as `interleave`.
## Pagelet and BigPipe classes
Although you can use the `HtmlStream` class directly, this project also comes with `Pagelet` and `BigPipe` classes that
offer a higher level API for working with `HtmlStream`. The idea is to break your page down into small "pagelets" that
know how to fetch their own data independently and render themselves. For example, you might have one pagelet that
fetches data from a profile service and knows how to render a user's profile, another pagelet that fetches data from an
ads service and knows how to render an ad unit, and so on. For each pagelet, you make your backend calls, get back
some `Future` (Scala) or `Promise` (Java) objects, render them into a `Future[Html]` or `Promise<Html>`, and then use
`new HtmlPagelet(id, future)` or `new HtmlPagelet(id, promise)` to wrap them in a `Pagelet` class. You can then compose
multiple `Pagelet` instances together using the `BigPipe` constructor.
The `BigPipe` instance you get back has a `render` method that you use to actually render your pagelets. The `render`
method processes your `Pagelets` as necessary for server-side or client-side rendering and gives you a `Map` from
`Pagelet` id to the `HtmlStream` for that `Pagelet`. In your template, you should extract the `HtmlStream` for each of
your `Pagelets` from this map and put it into the proper place in the markup:
```html
@bigPipe.render { pagelets =>
<h2>The foo pagelet should go here</h2>
<div>@pagelets(fooPagelet.id)</div>
<h2>The bar pagelet should go here</h2>
<div>@pagelets(barPagelet.id)</div>
}
```
When doing server-side rendering, the `HtmlStream` you get back from the `pagelets` `Map` will contain the fully
rendered HTML. When doing client-side rendering, the `HtmlStream` will instead contain an empty placeholder that looks
something like this:
```html
<div id="foo-pagelet"></div>
```
The actual content for your `Pagelet` will be streamed down at the very end (ie, at the bottom of all the markup you
pass to the `BigPipe.render` method) and it will be wrapped in markup that makes it invisible when it first arrives in
the browser. It will also include some JavaScript that knows how to extract the content and inject it into the right
placeholder in the DOM. This is what allows the pagelets to be sent down in any order, but still render correctly on
the page. The markup sent back by each `Pagelet` is in
[com.ybrikman.bigpipe.pagelet.scala.html](big-pipe/src/main/twirl/com/ybrikman/bigpipe/pageletClientSide.scala.html)
and looks roughly like this:
```html
<code id="pagelet1"><!--Your content--></code>
<script>BigPipe.onPagelet("pagelet1");</script>
```
The `BigPipe.onPagelet` method is part of [big-pipe.js](big-pipe/src/main/resources/public/com/ybrikman/ping/big-pipe.js),
so make sure to include that script on every page.
## big-pipe.js
The `BigPipe.onPagelet` method will extract the content from the `code` tag and call `BigPipe.renderPagelet` to render
it client-side into the DOM node with the specified id (e.g. `pagelet1` in the example above). The default
`BigPipe.renderPagelet` just inserts the content into the DOM using the `innerHTML` method. If you wish to use a more
sophisticated method for client-side rendering, simply override the `BigPipe.renderPagelet` with your own:
```javascript
BigPipe.renderPagelet = function(id, content) {
// Provide a custom way to insert the specified content into the DOM node with the given id
}
```
The `id` parameter will be the id of the DOM node and `content` will be your content. Note that if your content was
JSON instead of HTML, `big-pipe.js` will automatically call `JSON.parse` on it before passing it to you. This can be
convenient if you use client-side templating.
## Client-side templating
You can use a client-side templating technology, such as mustache.js or handlebars.js to render most of your page
in the browser. To do that, all you need to do is create a `Pagelet` that contains JSON (a `JsValue` for Scala
developers or `JsonNode` for Java developers) instead of HTML:
```scala
class MoreBigPipeExamples(serviceClient: FakeServiceClient) extends Controller {
/**
* Instead of rendering each pagelet server-side with Play's templating, you can send back JSON and render each
* pagelet with a client-side templating library such as mustache.js
*
* @return
*/
def clientSideTemplating = Action {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
val profileFuture = serviceClient.fakeRemoteCallJsonMedium("profile")
val graphFuture = serviceClient.fakeRemoteCallJsonMedium("graph")
val feedFuture = serviceClient.fakeRemoteCallJsonSlow("feed")
val inboxFuture = serviceClient.fakeRemoteCallJsonSlow("inbox")
val adsFuture = serviceClient.fakeRemoteCallJsonFast("ads")
val searchFuture = serviceClient.fakeRemoteCallJsonFast("search")
// Convert each Future into a Pagelet which will send the JSON to the browser as soon as it's available
val profile = JsonPagelet("profile", profileFuture)
val graph = JsonPagelet("graph", graphFuture)
val feed = JsonPagelet("feed", feedFuture)
val inbox = JsonPagelet("inbox", inboxFuture)
val ads = JsonPagelet("ads", adsFuture)
val search = JsonPagelet("search", searchFuture)
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
val bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search)
Ok.chunked(views.stream.clientSideTemplating(bigPipe, profile, graph, feed, inbox, ads, search))
}
}
```
Next, create your custom `BigPipe.renderPagelet` method:
```javascript
// Override the original BigPipe.renderPagelet method with one that uses mustache.js for client-side rendering
BigPipe.renderPagelet = function(id, json) {
var domElement = document.getElementById(id);
if (domElement) {
domElement.innerHTML = Mustache.render(template, json);
} else {
console.log("ERROR: cannot render pagelet because DOM node with id " + id + " does not exist");
}
};
```
See the `clientSideTemplating` method in
[controllers/MoreBigPipeExamples.scala](sample-app-scala/app/controllers/MoreBigPipeExamples.scala) (Scala developers) or
[controllers/MoreBigPipeExamples.java](sample-app-java/app/controllers/MoreBigPipeExamples.java) (Java developers) and
[big-pipe-with-mustache.js](sample-app-common/src/main/resources/public/javascripts/big-pipe-with-mustache.js) for working examples.
## Composing independent pagelets
TODO: write documentation
## De-duping remote service calls
If your page is built out of composable, independent pagelets, then each pagelet will know how to fetch all the data it
needs from backend services. If each pagelet is truly independent, that means you may have duplicated service calls.
For example, several pagelets may make the exact same backend call to fetch the current user's profile. This is
inefficient and increases the load on downstream services.
This project comes with a `DedupingCache` library that makes it easy to *de-dupe* service calls. You can use it to
ensure that if several pagelets request the exact same data, you only make one call to a backend service, and all the
other calls get the same cached response. This class has a single method called `get` that takes a key and a way to
generate the value for that key if it isn't already in the cache.
For example, if you are using Play's `WSClient` to make remote calls, you could wrap any calls to it with this `get`
method to ensure that any duplicate calls for a given URL get back a cached value:
```scala
class ServiceClient {
val cache = new DedupingCache[String, Future[WSResponse]]
def makeRequest(url: String): Future[WSResponse] = {
cache.get(url, wsClient.url(url).get())
}
}
```
See [controllers/Deduping.scala](sample-app-scala/app/controllers/Deduping.scala) (Scala developers) or
[controllers/Deduping.java](sample-app-java/app/controllers/Deduping.java) (Java developers) for a complete example of
how to setup and use the `DedupingCache`. You will also have to add the `CacheFilter` to your filter chain, as shown in
[loader/PingApplicationLoader.scala](sample-app-scala/app/loader/PingApplicationLoader.scala) (Scala developers) or
[loader/Filters.java](sample-app-java/app/loader/Filters.java) (Java developers).
# FAQ
## What are the caveats and drawbacks to BigPipe?
BigPipe is not for everyone. There are some serious drawbacks and caveats you should be aware of before using it:
### HTTP headers and error handling
With BigPipe streaming, you typically start sending the response back to the browser before your backend calls are
finished. The first part of that response is the HTTP headers and once you've sent them back to the browser, it's too
late to change your mind. If one of those backend calls fails, you've already sent your 200 OK, so you can no longer
just send the browser a 500 error or a redirect!
Instead, you must handle errors by injecting JavaScript code into your stream that displays the message when it arrives
in the browser or redirects the user as necessary. See the `errorHandling` method in
[controllers/MoreBigPipeExamples.scala](sample-app-scala/app/controllers/MoreBigPipeExamples.scala) (Scala developers) or
[controllers/MoreBigPipeExamples.java](sample-app-java/app/controllers/MoreBigPipeExamples.java) (Java developers) for
a working example.
### Caching
Because of the the way headers and error handling work, be extra careful using BigPipe if you cache entire
pages, especially at the CDN level. Otherwise, you may stream out a 200 OK to the CDN, hit an error with a backend call,
and accidentally end up caching a page with an error on it.
If your pages are mostly static and can be cached for a long time (e.g. blogs), BigPipe is probably not for you. If
your pages are mostly dynamic and cannot be cached (e.g. the news feeds at Facebook, LinkedIn, Twitter), then BigPipe
can help.
### Pop-in
Pagelets can be sent down to the browser and rendered client-side in any order. Therefore, you have to be careful to
avoid too much "pop-in", where rendering each pagelet causes random parts of the page to pop in and move around, which
makes the page hard to use.
To avoid annoying your users, use CSS to size the placeholder elements appropriately so they don't resize or move much
as the actual content pops in. Alternatively, use JavaScript to ensure that the elements on a page render from top to
bottom, even if they show up in a different order (e.g. set `display: none` until all the pagelets above the current
one have been filled in).
## Why not AJAX?
You could try to accomplish something similar to BigPipe by sending back a page that's empty and makes lots of AJAX
calls to fill in each pagelet. This approach is much slower than BigPipe for a number of reasons:
1. Each AJAX call requires an extra roundtrip to your server, which adds a lot of latency. This latency is especially
bad on mobile or slower connections.
2. Each extra roundtrip also increases the load on your server. Instead of 1 QPS to load a page, you now have 6 QPS to
load a page with 6 pagelets.
3. Older browsers severly limit how many AJAX calls you can do and most browsers give AJAX calls a low priority during
the initial page load.
4. You have to download, parse, and execute a bunch of JavaScript code before you can even make the AJAX calls.
5. It only works with JavaScript enabled.
BigPipe gives you all the benefits of an AJAX portal, but without the downsides, by using a single connection—that
is, the original connection used to request the page—and streaming down each pagelet using
[HTTP Chunked Encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding), which works in almost all browsers.
## Where can I find more info?
1. [Composable and Streamable Play Apps](https://engineering.linkedin.com/play/composable-and-streamable-play-apps):
a talk that introduces how BigPipe streaming works on top of Play (see the
[video](https://www.youtube.com/watch?v=4b1XLka0UIw) and
[slides](http://www.slideshare.net/brikis98/composable-and-streamable-play-apps)).
2. [BigPipe: Pipelining web pages for high performance](https://www.facebook.com/note.php?note_id=389414033919): the
original blog post by Facebook that introduces BigPipe on PHP.
3. [New technologies for the new LinkedIn home page](http://engineering.linkedin.com/frontend/new-technologies-new-linkedin-home-page):
the new LinkedIn homepage is using BigPipe style streaming with Play. This ping-play project is loosely based off of
the work done originally at LinkedIn.
# Project info
## Status
This project is in alpha status. It has been used on small projects and is reasonably well coded, tested, and
documented, but it needs more real world usage before it can be considered a mature library. Until the project hits
version 1.0.0, backwards compatibility is *not* guaranteed, so expect APIs to change.
## Contributing
Contributions in the form of bug reports and pull requests are very welcome.
Check out the [help wanted label](https://github.com/brikis98/ping-play/labels/help%20wanted)
for ideas.
Also, if you're using this project in production, [drop me a line](mailto:jim@ybrikman.com),
as I'd love to hear about your experiences!
## Changelog
### 0.13 (10/22/15)
* Fix issue where the pagelet body was not being escaped correctly
### 0.12 (07/06/15)
* Added support for server-side rendering.
* Refactored the `Pagelet` API into a trait and subclasses
* Added the `BigPipe` class for composing and rendering `Pagelets`
### 0.11 (06/30/15)
* First public release.
## Release process
This project is published to Sonatype as described in the
[SBT Deploying to Sonatype](http://www.scala-sbt.org/release/docs/Using-Sonatype.html) documentation. To do that, this
project uses the [sbt-sonatype](https://github.com/xerial/sbt-sonatype), [sbt-pgp](http://www.scala-sbt.org/sbt-pgp),
and [sbt-release](https://github.com/sbt/sbt-release) plugins.
To release a new version:
1. Add an entry to the [Changelog](#Changelog) in this README.
2. Make sure your PGP keys are setup
([docs here](http://www.scala-sbt.org/release/docs/Using-Sonatype.html#First+-+PGP+Signatures))
3. Run the SBT `release` command:
```
activator shell
set credentials += Credentials("Sonatype Nexus Repository Manager", "oss.sonatype.org", "<username>", "<password>")
release
```
Currently, only the maintainer, [Yevgeniy Brikman](http://www.ybrikman.com) has the credentials for publishing new
versions.
## TODO
1. The implementation, tests, and documentation for "composing" pagelets are not
yet finished. See [#18](https://github.com/brikis98/ping-play/issues/18). Note, you can find an implementation of composable
pagelets in the [splink/pagelets](https://github.com/splink/pagelets) library. Perhaps the two can be merged?
2. There are a number of feature requests. See the [enhancement label](https://github.com/brikis98/ping-play/labels/enhancement)
in issues.
# License
This code is available under the MIT license. See the LICENSE file for more info.
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/BigPipe.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import play.libs.HttpExecution;
import scala.collection.JavaConverters;
import scala.concurrent.ExecutionContext;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* This class composes the given Pagelets together and prepares them for either out-of-order client-side rendering (if
* renderOptions is set to ClientSide) or in-order server-side rendering (if renderOptions is set to ServerSide). Use
* the render method in this class in your templates to actually render the Pagelets. It provides you a Map from
* Pagelet id to the HtmlStream for that Pagelet. Insert the HtmlStream in this Map for each Pagelet into the
* appropriate part of your markup.
*/
public class BigPipe extends com.ybrikman.ping.scalaapi.bigpipe.BigPipe {
public BigPipe(PageletRenderOptions renderOptions, List<Pagelet> pagelets, ExecutionContext ec) {
super(renderOptions, toScalaPagelets(pagelets, ec), ec);
}
public BigPipe(PageletRenderOptions renderOptions, List<Pagelet> pagelets) {
this(renderOptions, pagelets, HttpExecution.defaultContext());
}
public BigPipe(PageletRenderOptions renderOptions, Pagelet ... pagelets) {
this(renderOptions, Arrays.asList(pagelets));
}
private static scala.collection.immutable.List<com.ybrikman.ping.scalaapi.bigpipe.Pagelet> toScalaPagelets(List<Pagelet> pagelets, ExecutionContext ec) {
List<com.ybrikman.ping.scalaapi.bigpipe.Pagelet> scalaPagelets =
pagelets
.stream()
.map(pagelet -> pagelet.wrapped(ec))
.collect(Collectors.toList());
return JavaConverters.asScalaBufferConverter(scalaPagelets).asScala().toList();
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlPagelet.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import play.libs.F;
import play.twirl.api.Html;
import scala.concurrent.ExecutionContext;
/**
* A Pagelet that contains HTML. Both server-side and client-side rendering are supported.
*/
public class HtmlPagelet implements Pagelet {
private final String id;
private final F.Promise<Html> content;
public HtmlPagelet(String id, F.Promise<Html> content) {
this.id = id;
this.content = content;
}
@Override
public String id() {
return id;
}
@Override
public com.ybrikman.ping.scalaapi.bigpipe.Pagelet wrapped(ExecutionContext ec) {
return new com.ybrikman.ping.scalaapi.bigpipe.HtmlPagelet(id, content.wrapped());
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlStreamHelper.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import com.ybrikman.ping.scalaapi.bigpipe.HtmlStream;
import com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;
import play.api.http.ContentTypeOf;
import play.api.http.Writeable;
import play.api.libs.iteratee.Enumerator;
import play.api.mvc.Codec;
import play.libs.F.Promise;
import play.libs.HttpExecution;
import play.mvc.Result;
import play.mvc.Results;
import play.twirl.api.Html;
import scala.collection.JavaConverters;
import scala.concurrent.ExecutionContext;
import java.util.Arrays;
import java.util.List;
public class HtmlStreamHelper {
public static HtmlStream empty() {
return HtmlStream.empty();
}
public static HtmlStream fromString(String str) {
return HtmlStream.fromString(str);
}
public static HtmlStream fromHtml(Html html) {
return HtmlStream.fromHtml(html);
}
public static HtmlStream fromHtmlEnumerator(Enumerator<Html> enumerator) {
return HtmlStream.fromHtmlEnumerator(enumerator);
}
public static HtmlStream fromHtmlPromise(Promise<Html> html) {
return fromHtmlPromise(html, HttpExecution.defaultContext());
}
public static HtmlStream fromHtmlPromise(Promise<Html> html, ExecutionContext ec) {
return HtmlStream.fromHtmlFuture(html.wrapped(), ec);
}
public static HtmlStream fromResult(Result result) {
return fromResult(result, HttpExecution.defaultContext(), Codec.utf_8());
}
public static HtmlStream fromResult(Result result, ExecutionContext ec, Codec codec) {
return HtmlStream.fromResult(result.toScala(), ec, codec);
}
public static HtmlStream fromResultPromise(Promise<Result> result) {
return fromResultPromise(result, HttpExecution.defaultContext());
}
public static HtmlStream fromResultPromise(Promise<Result> result, ExecutionContext ec) {
return HtmlStream.fromResultFuture(result.map(Result::toScala, ec).wrapped(), ec);
}
public static HtmlStream flatten(Promise<HtmlStream> stream) {
return flatten(stream, HttpExecution.defaultContext());
}
public static HtmlStream flatten(Promise<HtmlStream> stream, ExecutionContext ec) {
return HtmlStream.flatten(stream.wrapped(), ec);
}
public static HtmlStream interleave(HtmlStream ... streams) {
return interleave(Arrays.asList(streams));
}
public static HtmlStream interleave(List<HtmlStream> streams) {
return HtmlStream.interleave(JavaConverters.asScalaBufferConverter(streams).asScala());
}
public static Results.Chunks<Html> toChunks(HtmlStream stream) {
return toChunks(stream, Codec.utf_8(), HttpExecution.defaultContext());
}
public static Results.Chunks<Html> toChunks(HtmlStream stream, Codec codec, ExecutionContext executionContext) {
return new Results.Chunks<Html>(Writeable.writeableOf_Content(codec, ContentTypeOf.contentTypeOf_Html(codec))) {
@Override
public void onReady(Out<Html> out) {
JavaAdapter.writeEnumeratorToOut(stream.enumerator(), out, executionContext);
}
};
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/JsonPagelet.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import com.fasterxml.jackson.databind.JsonNode;
import com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;
import play.libs.F;
import scala.concurrent.ExecutionContext;
/**
* A Pagelet that contains JSON. The general usage pattern is to send this JSON to the browser and render it using a
* client-side templating language, such as Mustache.js. Therefore, this Pagelet only supports client-side rendering
* and will throw an exception if you try to render it server-side.
*/
public class JsonPagelet implements Pagelet {
private final String id;
private final F.Promise<JsonNode> content;
public JsonPagelet(String id, F.Promise<JsonNode> content) {
this.id = id;
this.content = content;
}
@Override
public String id() {
return id;
}
@Override
public com.ybrikman.ping.scalaapi.bigpipe.Pagelet wrapped(ExecutionContext ec) {
return new com.ybrikman.ping.scalaapi.bigpipe.JsonPagelet(id, content.map(JavaAdapter::jsonNodeToJsValue, ec).wrapped());
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/Pagelet.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import com.ybrikman.ping.scalaapi.bigpipe.HtmlStream;
import play.libs.HttpExecution;
import scala.concurrent.ExecutionContext;
/**
* The base interface for "pagelets", which represent small, self-contained pieces of a page that can be rendered
* independently.
*/
public interface Pagelet extends com.ybrikman.ping.scalaapi.bigpipe.Pagelet {
default public HtmlStream renderPlaceholder() {
return renderPlaceholder(HttpExecution.defaultContext());
}
default public HtmlStream renderServerSide() {
return renderServerSide(HttpExecution.defaultContext());
}
default public HtmlStream renderClientSide() {
return renderClientSide(HttpExecution.defaultContext());
}
@Override
default public HtmlStream renderPlaceholder(ExecutionContext ec) {
return wrapped(ec).renderPlaceholder(ec);
}
@Override
default public HtmlStream renderServerSide(ExecutionContext ec) {
return wrapped(ec).renderServerSide(ec);
}
@Override
default public HtmlStream renderClientSide(ExecutionContext ec) {
return wrapped(ec).renderClientSide(ec);
}
com.ybrikman.ping.scalaapi.bigpipe.Pagelet wrapped(ExecutionContext ec);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletContentType.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
/**
* The supported content types that you can have in a pagelet for BigPipe style streaming.
*/
public enum PageletContentType {
json,
html,
text
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletRenderOptions.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
/**
* Specify the type of rendering you wish to use with BigPipe: either client-side, out-of-order rendering, which
* gives the minimal load time for your page, but requires JavaScript, or server-side, in-order rendering, which has
* a higher load time (albeit still faster than not using BigPipe at all), but does not rely on JavaScript.
*/
public enum PageletRenderOptions {
ClientSide,
ServerSide
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/TextPagelet.java
================================================
package com.ybrikman.ping.javaapi.bigpipe;
import play.libs.F;
import scala.concurrent.ExecutionContext;
/**
* A Pagelet that contains plain text. Both server-side and client-side rendering are supported.
*/
public class TextPagelet implements Pagelet {
private final String id;
private final F.Promise<String> content;
public TextPagelet(String id, F.Promise<String> content) {
this.id = id;
this.content = content;
}
@Override
public String id() {
return id;
}
@Override
public com.ybrikman.ping.scalaapi.bigpipe.Pagelet wrapped(ExecutionContext ec) {
return new com.ybrikman.ping.scalaapi.bigpipe.TextPagelet(id, content.wrapped());
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/CacheFilter.java
================================================
package com.ybrikman.ping.javaapi.dedupe;
import com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;
import com.ybrikman.ping.scalaapi.dedupe.BeforeAndAfterFilter;
import play.libs.HttpExecution;
import play.mvc.Http;
import scala.concurrent.ExecutionContext;
/**
* Any time you use the DedupingCache, you must add this CacheFilter to your filter chain. This filter will initialize
* the cache for each incoming request and cleanup the cache after you're done processing the request. To avoid memory
* leaks, you want to be sure this filter runs on every single request, so it's a good idea to make it the very first
* one in the filter chain, so no other filter can bypass it.
*
* @param <K>
* @param <V>
*/
public class CacheFilter<K, V> extends BeforeAndAfterFilter {
public CacheFilter(DedupingCache<K, V> cache) {
this(cache, HttpExecution.defaultContext());
}
public CacheFilter(DedupingCache<K, V> cache, ExecutionContext executionContext) {
super(
JavaAdapter.javaConsumerToScalaFunction(rh -> cache.initCacheForRequest(contextFromRequestHeader(rh))),
JavaAdapter.javaConsumerToScalaFunction(rh -> cache.cleanupCacheForRequest(contextFromRequestHeader(rh))),
executionContext);
}
private static Http.Context contextFromRequestHeader(play.api.mvc.RequestHeader rh) {
return new Http.Context(new Http.RequestImpl(rh));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/DedupingCache.java
================================================
package com.ybrikman.ping.javaapi.dedupe;
import com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;
import com.ybrikman.ping.scalaapi.dedupe.Cache;
import com.ybrikman.ping.scalaapi.dedupe.CacheNotInitializedException;
import play.mvc.Http;
import javax.inject.Singleton;
import java.util.function.Supplier;
/**
* A cache you can use to de-dupe expensive calculations to ensure that the same calculation happens at most once while
* processing any incoming request. For example, imagine you get an incoming request to /foo, and to render this page,
* you have to make a dozen calls to remote services (e.g. a profile service, a search service, etc) using REST over
* HTTP. You can use the DedupingCache to ensure you don't make the same exact call more than once (e.g. fetch the
* exact same profile multiple times) by running all the calls through this client. You would use the URL of the REST
* call as the key and the Future returned from the call as the value:
*
* String remoteUrl = "http://example.com/some/remote/service";
* Promise<Response> promise = dedupingCache.get(remoteUrl, WS.url(remoteUrl).get());
*
* While processing any incoming request, using the code above ensures that you will not make multiple calls to the
* exact same remoteUrl; any duplicates will just return a Promise object that is already in the cache.
*
* You should only use the DedupingCache for data that is safe to cache. For example, HTTP GET calls are usually safe
* to cache since they should be idempotent, but HTTP POST calls are not safe to cache. Also, you need to add the
* CacheFilter to your filter chain so that it can clean up the cache after you're done processing an incoming request.
* Otherwise, you'll have a memory leak.
*
* @param <K> The type to use for keys. This type must define an equals and hashCode method. For example, if you're
* making REST over HTTP calls, the HTTP URL is a good key.
* @param <V> The type of value that will be returned. For example, if you're making REST over HTTP calls using Play's
* WS library, a Promise<Response> might be a good type for the value.
*/
@Singleton
public class DedupingCache<K, V> {
private final Cache<Long, Cache<K, V>> cache;
public DedupingCache() {
cache = new Cache<>();
}
/**
* Get the value for key K from the cache. If the value is not already in teh cache, use the valueIfMissing function
* to calculate a value, store it in the cache, and return that value.
*
* @param key
* @param valueIfMissing
* @return
*/
public V get(K key, Supplier<V> valueIfMissing) {
return get(key, valueIfMissing, Http.Context.current());
}
/**
* Get the value for key K from the cache. If the value is not already in teh cache, use the valueIfMissing function
* to calculate a value, store it in the cache, and return that value. This version of the method allows you to
* explicitly specify the HTTP context.
*
* @param key
* @param valueIfMissing
* @param context
* @return
*/
public V get(K key, Supplier<V> valueIfMissing, Http.Context context) {
return getCacheForPlayRequest(context).getOrElseUpdate(key, JavaAdapter.javaSupplierToScalaFunction(valueIfMissing));
}
/**
* Initialize the cache for the given incoming request. Should only be used by the CacheFilter.
*
* @param context
*/
public void initCacheForRequest(Http.Context context) {
cache.put(context.id(), new Cache<>());
}
/**
* Cleanup the cache after you're completely done processing an incoming request. This is necessary to prevent memory
* leaks. Should only be used by the CacheFilter.
*
* @param context
*/
public void cleanupCacheForRequest(Http.Context context) {
cache.remove(context.id());
}
private Cache<K, V> getCacheForPlayRequest(Http.Context context) {
return cache.get(context.id()).getOrElse(JavaAdapter.javaSupplierToScalaFunction(() -> {
throw new CacheNotInitializedException(
"No cache found for request with id " + context.id() + " Did you add " +
CacheFilter.class.getName() + " to your filter chain?");
}));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function2.java
================================================
package com.ybrikman.ping.javaapi.promise;
@FunctionalInterface
public interface Function2<A, B, R> {
public R apply(A a, B b);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function3.java
================================================
package com.ybrikman.ping.javaapi.promise;
@FunctionalInterface
public interface Function3<A, B, C, R> {
public R apply(A a, B b, C c);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function4.java
================================================
package com.ybrikman.ping.javaapi.promise;
@FunctionalInterface
public interface Function4<A, B, C, D, R> {
public R apply(A a, B b, C c, D d);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function5.java
================================================
package com.ybrikman.ping.javaapi.promise;
@FunctionalInterface
public interface Function5<A, B, C, D, E, R> {
public R apply(A a, B b, C c, D d, E e);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function6.java
================================================
package com.ybrikman.ping.javaapi.promise;
@FunctionalInterface
public interface Function6<A, B, C, D, E, F, R> {
public R apply(A a, B b, C c, D d, E e, F f);
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise2.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F;
public class Promise2<A, B> {
private final F.Promise<A> a;
private final F.Promise<B> b;
public Promise2(F.Promise<A> a, F.Promise<B> b) {
this.a = a;
this.b = b;
}
public <R> F.Promise<R> map(Function2<A, B, R> function) {
return a.flatMap(a -> b.map(b -> function.apply(a, b)));
}
public <R> F.Promise<R> flatMap(Function2<A, B, F.Promise<R>> function) {
return a.flatMap(a -> b.flatMap(b -> function.apply(a, b)));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise3.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F;
public class Promise3<A, B, C> {
private final F.Promise<A> a;
private final F.Promise<B> b;
private final F.Promise<C> c;
public Promise3(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c) {
this.a = a;
this.b = b;
this.c = c;
}
public <R> F.Promise<R> map(Function3<A, B, C, R> function) {
return a.flatMap(a -> b.flatMap(b -> c.map(c -> function.apply(a, b, c))));
}
public <R> F.Promise<R> flatMap(Function3<A, B, C, F.Promise<R>> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> function.apply(a, b, c))));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise4.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F;
public class Promise4<A, B, C, D> {
private final F.Promise<A> a;
private final F.Promise<B> b;
private final F.Promise<C> c;
private final F.Promise<D> d;
public Promise4(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c, F.Promise<D> d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
public <R> F.Promise<R> map(Function4<A, B, C, D, R> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.map(d -> function.apply(a, b, c, d)))));
}
public <R> F.Promise<R> flatMap(Function4<A, B, C, D, F.Promise<R>> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.flatMap(d -> function.apply(a, b, c, d)))));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise5.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F;
public class Promise5<A, B, C, D, E> {
private final F.Promise<A> a;
private final F.Promise<B> b;
private final F.Promise<C> c;
private final F.Promise<D> d;
private final F.Promise<E> e;
public Promise5(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c, F.Promise<D> d, F.Promise<E> e) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
}
public <R> F.Promise<R> map(Function5<A, B, C, D, E, R> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.flatMap(d -> e.map(e -> function.apply(a, b, c, d, e))))));
}
public <R> F.Promise<R> flatMap(Function5<A, B, C, D, E, F.Promise<R>> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.flatMap(d -> e.flatMap(e -> function.apply(a, b, c, d, e))))));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise6.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F;
public class Promise6<A, B, C, D, E, F> {
private final play.libs.F.Promise<A> a;
private final play.libs.F.Promise<B> b;
private final play.libs.F.Promise<C> c;
private final play.libs.F.Promise<D> d;
private final play.libs.F.Promise<E> e;
private final play.libs.F.Promise<F> f;
public Promise6(play.libs.F.Promise<A> a, play.libs.F.Promise<B> b, play.libs.F.Promise<C> c, play.libs.F.Promise<D> d, play.libs.F.Promise<E> e, play.libs.F.Promise<F> f) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
}
public <R> play.libs.F.Promise<R> map(Function6<A, B, C, D, E, F, R> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.flatMap(d -> e.flatMap(e -> f.map(f -> function.apply(a, b, c, d, e, f)))))));
}
public <R> play.libs.F.Promise<R> flatMap(Function6<A, B, C, D, E, F, play.libs.F.Promise<R>> function) {
return a.flatMap(a -> b.flatMap(b -> c.flatMap(c -> d.flatMap(d -> e.flatMap(e -> f.flatMap(f -> function.apply(a, b, c, d, e, f)))))));
}
}
================================================
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/PromiseHelper.java
================================================
package com.ybrikman.ping.javaapi.promise;
import play.libs.F.Promise;
public class PromiseHelper {
public static <A, B> Promise2<A, B> sequence(Promise<A> a, Promise<B> b) {
return new Promise2<>(a, b);
}
public static <A, B, C> Promise3<A, B, C> sequence(Promise<A> a, Promise<B> b, Promise<C> c) {
return new Promise3<>(a, b, c);
}
public static <A, B, C, D> Promise4<A, B, C, D> sequence(Promise<A> a, Promise<B> b, Promise<C> c, Promise<D> d) {
return new Promise4<>(a, b, c, d);
}
public static <A, B, C, D, E> Promise5<A, B, C, D, E> sequence(Promise<A> a, Promise<B> b, Promise<C> c, Promise<D> d, Promise<E> e) {
return new Promise5<>(a, b, c, d, e);
}
public static <A, B, C, D, E, F> Promise6<A, B, C, D, E, F> sequence(Promise<A> a, Promise<B> b, Promise<C> c, Promise<D> d, Promise<E> e, Promise<F> f) {
return new Promise6<>(a, b, c, d, e, f);
}
}
================================================
FILE: big-pipe/src/main/resources/public/com/ybrikman/ping/big-pipe.js
================================================
/**
* JavaScript helper functions for BigPipe-style streaming. This code has no external dependencies.
*/
(function() {
"use strict";
var JSON_CONTENT_TYPE = "json";
var HTML_CONTENT_TYPE = "html";
var TEXT_CONTENT_TYPE = "text";
var root = this;
var BigPipe = root.BigPipe = {};
var document = root.document;
var console = root.console;
var log = function(msg) {
if (console && console.log) {
console.log(msg);
}
};
BigPipe.unescapeForEmbedding = function(str) {
if (str) {
return str.replace(new RegExp('\\\\u002d\\\\u002d', "gi"), '--');
} else {
return str;
}
};
BigPipe.readEmbeddedContentFromDom = function(domId) {
var contentElem = document.getElementById(domId);
if (contentElem) {
return BigPipe.unescapeForEmbedding(contentElem.firstChild.nodeValue);
} else {
log("ERROR: Unable to read content from DOM node with id " + domId + " so return an empty String.");
return "";
}
};
BigPipe.parseEmbeddedJsonFromDom = function(domId) {
var content = BigPipe.readEmbeddedContentFromDom(domId);
return JSON.parse(content);
};
BigPipe.renderPagelet = function(id, content) {
var domElement = document.getElementById(id);
if (domElement) {
domElement.innerHTML = content;
} else {
log("ERROR: cannot insert pagelet content because DOM node with id " + id + " does not exist");
}
};
BigPipe.onPagelet = function(id, contentId, contentType) {
if (contentType === JSON_CONTENT_TYPE) {
var json = BigPipe.parseEmbeddedJsonFromDom(contentId);
BigPipe.renderPagelet(id, json);
} else if (contentType === HTML_CONTENT_TYPE || contentType === TEXT_CONTENT_TYPE) {
var content = BigPipe.readEmbeddedContentFromDom(contentId);
BigPipe.renderPagelet(id, content);
} else {
log("ERROR: unsupported contentType " + contentType + " for pagelet with id " + id);
}
};
}.call(this));
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/BigPipe.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions._
import scala.concurrent.ExecutionContext
/**
* This class composes the given Pagelets together and prepares them for either out-of-order client-side rendering (if
* renderOptions is set to ClientSide) or in-order server-side rendering (if renderOptions is set to ServerSide). Use
* the render method in this class in your templates to actually render the Pagelets. It provides you a Map from
* Pagelet id to the HtmlStream for that Pagelet. Insert the HtmlStream in this Map for each Pagelet into the
* appropriate part of your markup.
*
* @param renderOptions
* @param pagelets
* @param ec
*/
class BigPipe(renderOptions: PageletRenderOptions, pagelets: Pagelet*)(implicit ec: ExecutionContext) {
/**
* Render the Pagelets in this BigPipe. The layoutBody function will get as an argument a Map from Pagelet id to
* HtmlStream for that Pagelet. Insert this HtmlStream into the appropriate place in your markup.
*
* @param layoutBody
* @return
*/
def render(layoutBody: Map[String, HtmlStream] => HtmlStream): HtmlStream = {
val bodyPagelets = pagelets.map { pagelet =>
renderOptions match {
case ClientSide => pagelet.id -> pagelet.renderPlaceholder
case ServerSide => pagelet.id -> pagelet.renderServerSide
}
}.toMap
val footerPagelets = renderOptions match {
case ClientSide => HtmlStream.interleave(pagelets.map(_.renderClientSide):_*)
case ServerSide => HtmlStream.empty
}
layoutBody(bodyPagelets).andThen(footerPagelets)
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/Embed.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import play.api.libs.json.{Json, JsValue}
import play.twirl.api.Html
object Embed {
/**
* Convert the given json into an HTML String that can be safely embedded into a webpage in a way that the browser
* completely ignores it.
*
* @param json
* @return
*/
def escapeForEmbedding(json: JsValue): Html = {
escapeForEmbedding(Json.stringify(json))
}
/**
* Convert the given HTML into an HTML String that can be safely embedded into a webpage in a way that the browser
* completely ignores it.
*
* @param html
* @return
*/
def escapeForEmbedding(html: Html): Html = {
escapeForEmbedding(html.body)
}
/**
* Convert the given String into an HTML String that can be safely embedded into a webpage in a way that the browser
* completely ignores it.
*
* @param str
* @return
*/
def escapeForEmbedding(str: String): Html = {
Html(escapeDashes(str))
}
/**
* To hide content from the browser, we wrap it in an HTML comment. To make sure no content can escape from that
* comment, the only thing we have to do is escape double dashes.
*
* @param str
* @return
*/
private def escapeDashes(str: String): String = {
str.replaceAll("--", "\\\\\\u002d\\\\\\u002d")
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/HtmlStream.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import play.api.libs.iteratee.{Enumeratee, Enumerator}
import play.twirl.api.{Appendable, HtmlFormat, Format, Html}
import play.api.mvc.{Codec, Result}
import scala.language.implicitConversions
import scala.concurrent.{ExecutionContext, Future}
/**
* A custom Appendable that lets you create .scala.stream templates instead of .scala.html. These templates can mix Html
* markup with Enumerators that contain Html markup so that as soon as the content is available, Play can stream it
* back to the client. You need to add this class as a custom template type in build.sbt.
*
* @param enumerator
*/
class HtmlStream(val enumerator: Enumerator[Html]) extends Appendable[HtmlStream] {
def andThen(other: HtmlStream): HtmlStream = HtmlStream.fromHtmlEnumerator(enumerator.andThen(other.enumerator))
}
/**
* Companion object for HtmlStream that contains convenient factory and composition methods.
*/
object HtmlStream {
/**
* Create an empty HtmlStream
*
* @return
*/
def empty: HtmlStream = {
fromString("")
}
/**
* Create an HtmlStream from a String
*
* @param text
* @return
*/
def fromString(text: String): HtmlStream = {
fromHtml(Html(text))
}
/**
* Create an HtmlStream from a Future that will eventually contain a String
*
* @param eventuallyString
* @return
*/
def fromStringFuture(eventuallyString: Future[String])(implicit ec: ExecutionContext): HtmlStream = {
fromHtmlFuture(eventuallyString.map(Html.apply))
}
/**
* Create an HtmlStream from Html
*
* @param html
* @return
*/
def fromHtml(html: Html): HtmlStream = {
fromHtmlEnumerator(Enumerator(html))
}
/**
* Create an HtmlStream from an Enumerator of Html
*
* @param enumerator
* @return
*/
def fromHtmlEnumerator(enumerator: Enumerator[Html]): HtmlStream = {
new HtmlStream(enumerator)
}
/**
* Create an HtmlStream from a Future that will eventually contain Html
*
* @param eventuallyHtml
* @return
*/
def fromHtmlFuture(eventuallyHtml: Future[Html])(implicit ec: ExecutionContext): HtmlStream = {
flatten(eventuallyHtml.map(fromHtml))
}
/**
* Create an HtmlStream from the body of the Result.
*
* @param result
* @return
*/
def fromResult(result: Result)(implicit ec: ExecutionContext, codec: Codec): HtmlStream = {
HtmlStream.fromHtmlEnumerator(result.body.map(bytes => Html(codec.decode(bytes))))
}
/**
* Create an HtmlStream from a the body of a Future[Result].
*
* @param result
* @return
*/
def fromResultFuture(result: Future[Result])(implicit ec: ExecutionContext): HtmlStream = {
flatten(result.map(fromResult))
}
/**
* Interleave multiple HtmlStreams together. Interleaving is done based on whichever HtmlStream next has input ready,
* if multiple have input ready, the order is undefined.
*
* @param streams
* @return
*/
def interleave(streams: HtmlStream*): HtmlStream = {
fromHtmlEnumerator(Enumerator.interleave(streams.map(_.enumerator)))
}
/**
* Create an HtmlStream from a Future that will eventually contain an HtmlStream.
*
* @param eventuallyStream
* @return
*/
def flatten(eventuallyStream: Future[HtmlStream])(implicit ec: ExecutionContext): HtmlStream = {
fromHtmlEnumerator(Enumerator.flatten(eventuallyStream.map(_.enumerator)))
}
}
/**
* A custom Appendable that lets you create .scala.stream templates instead of .scala.html. These templates can mix Html
* markup with Enumerators that contain Html markup so that as soon as the content is available, Play can stream it
* back to the client.
*/
object HtmlStreamFormat extends Format[HtmlStream] {
def raw(text: String): HtmlStream = {
HtmlStream.fromString(text)
}
def escape(text: String): HtmlStream = {
raw(HtmlFormat.escape(text).body)
}
def empty: HtmlStream = {
raw("")
}
def fill(elements: scala.collection.immutable.Seq[HtmlStream]): HtmlStream = {
elements.reduce((agg, curr) => agg.andThen(curr))
}
}
/**
* Useful implicits when working with HtmlStreams
*/
object HtmlStreamImplicits {
/**
* Implicit conversion so HtmlStream can be passed directly to Ok.feed and Ok.chunked
*
* @param stream
* @param ec
* @return
*/
implicit def toEnumerator(stream: HtmlStream)(implicit ec: ExecutionContext): Enumerator[Html] = {
// Skip empty chunks, as these mean EOF in chunked encoding
stream.enumerator.through(Enumeratee.filter(!_.body.isEmpty))
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/JavaAdapter.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import java.util.function.{Supplier => JavaSupplier, Consumer => JavaConsumer, Function => JavaFunction}
import com.fasterxml.jackson.databind.JsonNode
import play.api.libs.iteratee.{Enumerator, Iteratee}
import play.api.libs.json.{Writes, JsValue}
import play.mvc.Results.Chunks.Out
import scala.concurrent.ExecutionContext
/**
* A helper class for going between Scala and Java code
*/
object JavaAdapter {
def writeEnumeratorToOut[A](enumerator: Enumerator[A], out: Out[A], executionContext: ExecutionContext): Unit = {
implicit val ec = executionContext
enumerator.run(Iteratee.foreach { chunk =>
if (!chunk.toString.isEmpty) {
out.write(chunk)
}
}).onComplete(_ => out.close())
}
def jsonNodeToJsValue(jsonNode: JsonNode): JsValue = {
Writes.JsonNodeWrites.writes(jsonNode)
}
def javaConsumerToScalaFunction[A](consumer: JavaConsumer[A]): A => Unit = {
(a) => consumer.accept(a)
}
def javaSupplierToScalaFunction[A](supplier: JavaSupplier[A]): () => A = {
() => supplier.get()
}
def javaFunctionToScalaFunction[A, B](function: JavaFunction[A, B]): A => B = {
(a) => function.apply(a)
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/Pagelet.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import com.ybrikman.ping.javaapi.bigpipe.PageletContentType
import play.api.libs.json.{JsValue, Json}
import play.twirl.api.Html
import scala.concurrent.{ExecutionContext, Future}
import PageletConstants._
/**
* The base trait for "pagelets", which represent small, self-contained pieces of a page that can be rendered
* independently.
*/
trait Pagelet {
/**
* A unique id for this Pagelet. Usually corresponds to the id in the DOM where this Pagelet should be inserted.
*/
val id: String
/**
* Render an HTML placeholder for this Pagelet. This will be filled in later using JavaScript code when the Pagelet
* data is available and shows up in the browser.
*
* @param ec
* @return
*/
def renderPlaceholder(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtml(com.ybrikman.bigpipe.html.pageletServerSide(id, EmptyContent))
}
/**
* Render all the HTML for this Pagelet server-side. This is typically used when the Pagelets are being streamed
* in-order, which is useful for clients that do not support JavaScript and search engine crawlers (i.e. SEO).
*
* @param ec
* @return
*/
def renderServerSide(implicit ec: ExecutionContext): HtmlStream
/**
* Render the HTML for this Pagelet so that it's initially invisible and can be inserted into the proper place in the
* DOM client-side, using JavaScript. This is typically used when the Pagelets are being streamed out-of-order to
* minimize the load-time for a page.
*
* @param ec
* @return
*/
def renderClientSide(implicit ec: ExecutionContext): HtmlStream
}
/**
* A Pagelet that contains HTML. Both server-side and client-side rendering are supported.
*
* @param id
* @param content
*/
case class HtmlPagelet(id: String, content: Future[Html]) extends Pagelet {
override def renderServerSide(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtmlFuture(content.map(str => com.ybrikman.bigpipe.html.pageletServerSide(id, str.body)))
}
override def renderClientSide(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtmlFuture(content.map(str =>
com.ybrikman.bigpipe.html.pageletClientSide(str.body, id, PageletContentType.html)))
}
}
/**
* A Pagelet that contains JSON. The general usage pattern is to send this JSON to the browser and render it using a
* client-side templating language, such as Mustache.js. Therefore, this Pagelet only supports client-side rendering
* and will throw an exception if you try to render it server-side.
*
* @param id
* @param content
*/
case class JsonPagelet(id: String, content: Future[JsValue]) extends Pagelet {
override def renderServerSide(implicit ec: ExecutionContext): HtmlStream = {
throw new UnsupportedOperationException(s"Server-side rendering is not supported for ${getClass.getName}")
}
override def renderClientSide(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtmlFuture(content.map(json =>
com.ybrikman.bigpipe.html.pageletClientSide(Json.stringify(json), id, PageletContentType.json)))
}
}
/**
* A Pagelet that contains plain text. Both server-side and client-side rendering are supported.
*
* @param id
* @param content
*/
case class TextPagelet(id: String, content: Future[String]) extends Pagelet {
override def renderServerSide(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtmlFuture(content.map(str => com.ybrikman.bigpipe.html.pageletServerSide(id, str)))
}
override def renderClientSide(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtmlFuture(content.map(str =>
com.ybrikman.bigpipe.html.pageletClientSide(str, id, PageletContentType.text)))
}
}
object PageletConstants {
val EmptyContent = ""
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/compose/Compose.scala
================================================
package com.ybrikman.ping.scalaapi.compose
import com.ybrikman.ping.scalaapi.bigpipe.HtmlStream
import play.api.http.HeaderNames
import play.api.libs.iteratee.Iteratee
import play.api.mvc.{Cookies, Cookie, Codec, Result}
import play.twirl.api.Html
import scala.concurrent.{Future, ExecutionContext}
/**
* Helpers for building Play apps out of standalone, composable pagelets.
* Note: these are not yet tested or documented, so use at your own risk.
*/
object Compose {
val cssHeaderName = "X-CSS"
val jsHeaderName = "X-JS"
/**
* Read the body of a Result as Html. Since the body is an Enumerator and may not be available yet, this method
* returns a Future.
*
* @param result
* @param codec
* @return
*/
def readBody(result: Result)(implicit codec: Codec, ec: ExecutionContext): Future[Html] = {
result.body.run(Iteratee.consume()).map(bytes => Html(new String(bytes, codec.charset)))
}
/**
* Merge all the cookies set in the given results into a single sequence.
*
* @param results
* @return
*/
def mergeCookies(results: Result*): Seq[Cookie] = {
results
.flatMap(result => result.header.headers.get(HeaderNames.SET_COOKIE)
.map(Cookies.decodeSetCookieHeader)
.getOrElse(Seq.empty))
}
/**
* Convert the given sequences of CSS and JS into HTTP headers that can be added to the Result
*
* @param css
* @param js
* @return
*/
def asHeaders(css: Seq[String], js: Seq[String]): Seq[(String, String)] = {
Seq(cssHeaderName -> css.mkString(","), jsHeaderName -> js.mkString(","))
}
/**
* Read the CSS header from each result and merge and de-dupe them into a single sequence
*
* @param results
* @return
*/
def mergeCssHeaders(results: Result*): Seq[String] = {
mergeHeaderValues(cssHeaderName, parseCssHeader, results:_*)
}
/**
* Read the JS header from each the result and merge and de-dupe them into a single sequence
*
* @param results
* @return
*/
def mergeJsHeaders(results: Result*): Seq[String] = {
mergeHeaderValues(jsHeaderName, parseJsHeader, results:_*)
}
private def mergeHeaderValues(headerName: String, parseHeader: Result => Seq[String], results: Result*): Seq[String] = {
results.flatMap(parseHeader).distinct
}
/**
* Read the CSS header from the given Result, which should define the CSS dependencies for the Result
*
* @param result
* @return
*/
def parseCssHeader(result: Result): Seq[String] = parseHeader(cssHeaderName, result)
/**
* Read the JS header from the given Result, which should define the CSS dependencies for the Result
*
* @param result
* @return
*/
def parseJsHeader(result: Result): Seq[String] = parseHeader(jsHeaderName, result)
/**
* Render the given sequence of CSS URLs as link tags
*
* @param css
* @return
*/
def renderCssDependencies(css: Seq[String]): Html = {
com.ybrikman.bigpipe.html.css(css)
}
/**
* Render the given sequence of JS URLs as script tags
*
* @param js
* @return
*/
def renderJsDependencies(js: Seq[String]): Html = {
com.ybrikman.bigpipe.html.js(js)
}
/**
* Merge all the JavaScript dependencies from the results into a list of script tags
*
* @param results
* @return
*/
def mergeJsFromResults(results: Future[Result]*)(implicit ec: ExecutionContext): HtmlStream = {
mergeDependenciesFromResults(parseJsHeader, renderJsDependencies, results)
}
/**
* Merge all the CSS dependencies from the results into a list of link tags
*
* @param results
* @return
*/
def mergeCssFromResults(results: Future[Result]*)(implicit ec: ExecutionContext): HtmlStream = {
mergeDependenciesFromResults(parseCssHeader, renderCssDependencies, results)
}
private def parseHeader(headerName: String, result: Result): Seq[String] = {
result.header.headers.get(headerName).map(_.split(",").toVector).getOrElse(Vector.empty)
}
private def mergeDependenciesFromResults(parseHeader: Result => Seq[String], render: Seq[String] => Html, resultFutures: Seq[Future[Result]])(implicit ec: ExecutionContext): HtmlStream = {
val allResultsFuture = Future.sequence(resultFutures)
val htmlFuture = allResultsFuture.map { results =>
val values = results.map(parseHeader).flatten.distinct
render(values)
}
HtmlStream.fromHtmlFuture(htmlFuture)
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/BeforeAndAfterFilter.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import play.api.mvc.{Result, RequestHeader, Filter}
import scala.concurrent.{ExecutionContext, Future}
/**
* A filter that takes two parameters--a before function and an after function--and guarantees the before function is
* executed before the rest of the filter chain executes and the after function is executed after the rest of the
* filter chain (no matter what error may have been thrown).
*
* @param before
* @param after
* @param ec
*/
class BeforeAndAfterFilter(before: RequestHeader => Unit, after: RequestHeader => Unit)(implicit ec: ExecutionContext) extends Filter {
override def apply(next: (RequestHeader) => Future[Result])(playRequest: RequestHeader): Future[Result] = {
// Be very careful with error handling to guarantee the after code executes no matter what kind of error happened.
try {
before(playRequest)
next(playRequest).map { result =>
result.copy(body = result.body.onDoneEnumerating(after(playRequest)))
}.recover { case t: Throwable =>
after(playRequest)
throw t
}
} catch {
case t: Throwable =>
after(playRequest)
throw t
}
}
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/Cache.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import Cache._
import play.api.Configuration
import java.util.concurrent.ConcurrentHashMap
import collection.JavaConverters._
/**
* A Scala wrapper for a Java's ConcurrentHashMap (CHM). Exposes the basic underlying methods of CHM and adds a
* getOrElseUpdate(key, value) method that lazily evaluates the value parameter only if the key is not already present
* in the cache.
*
* You may be asking, why not just use Scala's ConcurrentMap interface, which already has a getOrElseUpdate method?
*
* val cache = new ConcurrentHashMap().asScala
* cache.getOrElseUpdate("foo", "bar") // BAD idea
*
* The answer is because this method is inherited from the MapLike trait, and is NOT a thread safe (atomic) operation!
*
* The strategy used in the class below is to wrap all values with a LazyWrapper class that only evaluates the value
* when explicitly accessed. In the getOrElseUpdate method, we avoid accessing the passed in value unless we know it
* was the one actually inserted into the cache.
*
* For more info, see: http://boundary.com/blog/2011/05/
*
* TODO: investigate if boundary's NonBlockingHashMap is as good as they say it is (and what tests they have to prove
* it).
*
* TODO: Java-friendly API
*
* @param initialCapacity
* @param concurrencyLevel
* @param loadFactor
* @tparam K
* @tparam V
*/
class Cache[K, V](initialCapacity: Int, loadFactor: Float, concurrencyLevel: Int) {
/**
* Overloaded constructor that creates the cache with initial capacity, concurrency level, and load factor read from
* config
*
* @param config
* @return
*/
def this(config: Configuration) = this(
config.getInt(CONFIG_KEY_INITIAL_CAPACITY).getOrElse(DEFAULT_INITIAL_CAPACITY),
config.getDouble(CONFIG_KEY_LOAD_FACTOR).map(_.toFloat).getOrElse(DEFAULT_LOAD_FACTOR),
config.getInt(CONFIG_KEY_CONCURRENCY_LEVEL).getOrElse(DEFAULT_CONCURRENCY_LEVEL)
)
/**
* Empty constructor that uses default values for initial capacity, concurrency level, and load factor
* @return
*/
def this() = this(
DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL
)
private val cache = new ConcurrentHashMap[K, LazyWrapper[V]](initialCapacity, loadFactor, concurrencyLevel).asScala
/**
* Returns all elements of the cache. Use this method only if you really need all of the elements. Calling it will cause
* all lazy values to be calculated.
*/
def getAll: Map[K, V] = {
val mutable = cache.map { case(key, wrapper) => key -> unwrap(wrapper) }
mutable.toMap
}
/**
* Returns true if this key is associated with a value in the cache and false otherwise.
*
* @param key
* @return
*/
def contains(key: K): Boolean = {
cache.contains(key)
}
/**
* Optionally return the value associated with the given key
*
* @param key
* @return
*/
def get(key: K): Option[V] = {
cache.get(key).map(unwrap)
}
/**
* Associate the given key with the given value. Optionally return any value previously associated with the key.
*
* @param key
* @param value
* @return
*/
def put(key: K, value: V): Option[V] = {
cache.put(key, wrap(value)).map(unwrap)
}
/**
* If the given key is already associated with a value, return that value. Otherwise, associate the key with the
* given value and return None.
*
* @param key
* @param value
* @return
*/
def putIfAbsent(key: K, value: V): Option[V] = {
cache.putIfAbsent(key, wrap(value)).map(unwrap)
}
/**
* Get the value associated with the given key. If no value is already associated, then associate the given value
* with the key and use it as the return value.
*
* Like Scala's ConcurrentMap, the value parameter will be lazily evaluated: that is, it'll only be evaluated if
* there wasn't already a value associated with the given key. However, unlike Scala's ConcurrentMap, this method is
* a thread safe (atomic) operation.
*
* @param key
* @param value
* @return
*/
def getOrElseUpdate(key: K, value: => V): V = {
val newWrapper = wrap(value)
// If there was no previous value, we'll end up calling the .value on newWrapper, which will evaluate it for the
// first (and last) time
cache.putIfAbsent(key, newWrapper).getOrElse(newWrapper).value
}
/**
* Remove the key and any associated value from the cache. Optionally return any previously associated value.
*
* @param key
* @return
*/
def remove(key: K): Option[V] = {
cache.remove(key).map(unwrap)
}
/**
* Remove all keys and values from the cache
*/
def clear() {
cache.clear()
}
/**
* Return how many elements are in the cache
*
* @return
*/
def size: Int = {
cache.size
}
private def wrap[T](value: => T): LazyWrapper[T] = {
new LazyWrapper[T](value)
}
private def unwrap[T](lazyWrapper: LazyWrapper[T]): T = {
lazyWrapper.value
}
override def toString: String = "Cache(%s)".format(cache)
override def hashCode(): Int = cache.hashCode()
override def equals(other: Any): Boolean = {
Option(other) match {
case Some(otherCache: Cache[_, _]) => cache.equals(otherCache.cache)
case _ => false
}
}
}
/**
* A wrapper that avoids evaluating the value until explicitly accessed by calling either .value, .equals, .hashCode,
* or .toString.
*
* @param wrapped
* @tparam T
*/
class LazyWrapper[T](wrapped: => T) {
// Store in a lazy val to make sure the wrapped value is evaluated at most once
lazy val value = wrapped
override def toString: String = "LazyWrapper(%s)".format(value)
override def hashCode(): Int = value.hashCode()
override def equals(other: Any): Boolean = {
Option(other) match {
case Some(otherLazy: LazyWrapper[_]) => value.equals(otherLazy.value)
case _ => false
}
}
}
object Cache {
val DEFAULT_INITIAL_CAPACITY = 16
val DEFAULT_CONCURRENCY_LEVEL = 16
val DEFAULT_LOAD_FACTOR = 0.75f
val CONFIG_KEY_INITIAL_CAPACITY = "initialCapacity"
val CONFIG_KEY_CONCURRENCY_LEVEL = "concurrencyLevel"
val CONFIG_KEY_LOAD_FACTOR = "loadFactor"
}
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/CacheFilter.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import scala.concurrent.ExecutionContext
/**
* Any time you use the DedupingCache, you must add this CacheFilter to your filter chain. This filter will initialize
* the cache for each incoming request and cleanup the cache after you're done processing the request. To avoid memory
* leaks, you want to be sure this filter runs on every single request, so it's a good idea to make it the very first
* one in the filter chain, so no other filter can bypass it.
*
* @param dedupingCache
* @tparam K
* @tparam V
*/
class CacheFilter[K, V](dedupingCache: DedupingCache[K, V])(implicit ec: ExecutionContext) extends BeforeAndAfterFilter(
before = rh => dedupingCache.initCacheForRequest(rh),
after = rh => dedupingCache.cleanupCacheForRequest(rh))
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/CacheNotInitializedException.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
class CacheNotInitializedException(message: String) extends RuntimeException(message)
================================================
FILE: big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/DedupingCache.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import play.api.mvc.RequestHeader
/**
* A cache you can use to de-dupe expensive calculations to ensure that the same calculation happens at most once while
* processing any incoming request. For example, imagine you get an incoming request to /foo, and to render this page,
* you have to make a dozen calls to remote services (e.g. a profile service, a search service, etc) using REST over
* HTTP. You can use the DedupingCache to ensure you don't make the same exact call more than once (e.g. fetch the
* exact same profile multiple times) by running all the calls through this client. You would use the URL of the REST
* call as the key and the Future returned from the call as the value:
*
* val remoteUrl = "http://example.com/some/remote/service"
* val future: Future[Response] = dedupingCache.get(remoteUrl, WS.url(remoteUrl).get())
*
* While processing any incoming request, using the code above ensures that you will not make multiple calls to the
* exact same remoteUrl; any duplicates will just return a Future object that is already in the cache.
*
* You should only use the DedupingCache for data that is safe to cache. For example, HTTP GET calls are usually safe
* to cache since they should be idempotent, but HTTP POST calls are not safe to cache. Also, you need to add the
* CacheFilter to your filter chain so that it can clean up the cache after you're done processing an incoming request.
* Otherwise, you'll have a memory leak.
*
* @tparam K The type to use for keys. This type must define an equals and hashCode method. For example, if you're
* making REST over HTTP calls, the HTTP URL is a good key.
* @tparam V The type of value that will be returned. For example, if you're making REST over HTTP calls using Play's
* WS library, a Future[Response] might be a good type for the value.
*/
class DedupingCache[K, V] {
private val cache = new Cache[Long, Cache[K, V]]()
/**
* Get the value for key K from the cache. If the value is not already in teh cache, use the valueIfMissing function
* to calculate a value, store it in the cache, and return that value.
*
* @param key
* @param valueIfMissing
* @param playRequest
* @return
*/
def get(key: K, valueIfMissing: => V)(implicit playRequest: RequestHeader): V = {
getCacheForPlayRequest(playRequest).getOrElseUpdate(key, valueIfMissing)
}
/**
* Initialize the cache for the given incoming request. Should only be used by the CacheFilter.
*
* @param playRequest
*/
def initCacheForRequest(playRequest: RequestHeader): Unit = {
cache.put(playRequest.id, new Cache[K, V])
}
/**
* Cleanup the cache after you're completely done processing an incoming request. This is necessary to prevent memory
* leaks. Should only be used by the CacheFilter.
*
* @param playRequest
*/
def cleanupCacheForRequest(playRequest: RequestHeader): Unit = {
cache.remove(playRequest.id)
}
private def getCacheForPlayRequest(playRequest: RequestHeader): Cache[K, V] = {
cache.get(playRequest.id).getOrElse(throw new CacheNotInitializedException(
s"No cache found for request with id ${playRequest.id}. " +
s"Did you add ${classOf[CacheFilter[_, _]].getName} to your filter chain?"))
}
}
================================================
FILE: big-pipe/src/main/twirl/com/ybrikman/bigpipe/css.scala.html
================================================
@(urls: Seq[String])
@for(url <- urls) {
<link rel="stylesheet" href="@url"/>
}
================================================
FILE: big-pipe/src/main/twirl/com/ybrikman/bigpipe/js.scala.html
================================================
@(urls: Seq[String])
@for(url <- urls) {
<script src="@url" type="text/javascript"></script>
}
================================================
FILE: big-pipe/src/main/twirl/com/ybrikman/bigpipe/pageletClientSide.scala.html
================================================
@(content: String, id: String, contentType: com.ybrikman.ping.javaapi.bigpipe.PageletContentType)
@import com.ybrikman.ping.scalaapi.bigpipe.Embed
@defining(s"$id-$contentType") { contentId =>
<code id="@contentId" class="bigpipe-embed-html"><!--@Embed.escapeForEmbedding(content)--></code>
<script>(function() { this.BigPipe.onPagelet("@id", "@contentId", "@contentType.name()"); }.call(this));</script>
}
================================================
FILE: big-pipe/src/main/twirl/com/ybrikman/bigpipe/pageletServerSide.scala.html
================================================
@(id: String, content: String)
<div id="@id">@Html(content)</div>
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/javaapi/dedupe/TestCacheFilter.scala
================================================
package com.ybrikman.ping.javaapi.dedupe
import java.util.function.Supplier
import com.ybrikman.ping.scalaapi.dedupe.CacheNotInitializedException
import org.specs2.mutable.Specification
import play.api.mvc.{Results, Result, RequestHeader}
import play.mvc.Http.{RequestBuilder, Context}
import play.api.test.Helpers._
import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
class TestCacheFilter extends Specification {
"The Java CacheFilter should" >> {
"initialize the cache before the filter chain and clean it up after the filter chain" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = new DedupingCache[String, String]
val filter = new CacheFilter(cache, defaultContext)
val expectedResult = "bar"
def next(rh: RequestHeader): Future[Result] = {
Future.successful(Results.Ok(cache.get("foo", supplier(expectedResult), fakeContext)))
}
val fakeRequest = fakeContext._requestHeader()
val actualResult = contentAsString(filter(next _)(fakeRequest))
actualResult mustEqual expectedResult
// Ensure cache was cleaned up for that request
cache.get("foo", supplier("shouldNotBeUsed"), fakeContext) must throwA[CacheNotInitializedException]
}
"initialize the cache before the filter chain and clean it up after the filter chain even if an exception is thrown" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = new DedupingCache[String, String]
val filter = new CacheFilter(cache, defaultContext)
val expectedResult = "bar"
def next(rh: RequestHeader): Future[Result] = {
throw new CacheFilterTestException(cache.get("foo", supplier(expectedResult), fakeContext))
}
val fakeRequest = fakeContext._requestHeader()
contentAsString(filter(next _)(fakeRequest)) must throwA[CacheFilterTestException](message = expectedResult)
// Ensure cache was cleaned up for that request
cache.get("foo", supplier("shouldNotBeUsed"), fakeContext) must throwA[CacheNotInitializedException]
}
}
private def supplier[A](value: => A): Supplier[A] = {
new Supplier[A] {
override def get(): A = value
}
}
}
class CacheFilterTestException(message: String) extends RuntimeException(message)
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/javaapi/dedupe/TestDedupingCache.scala
================================================
package com.ybrikman.ping.javaapi.dedupe
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Supplier
import com.ybrikman.ping.scalaapi.dedupe.CacheNotInitializedException
import org.specs2.mutable.Specification
import play.mvc.Http.{RequestBuilder, Context}
class TestDedupingCache extends Specification {
"The Java DedupingCache get method should" >> {
"throw an exception if the cache is not initialized" >> {
val uninitializedCache = new DedupingCache[String, Integer]
val fakeContext = new Context(new RequestBuilder)
uninitializedCache.get("foo", supplier(1), fakeContext) must throwA[CacheNotInitializedException]
}
"return valueIfMissing when the cache is empty" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = createInitializedCache[String, Int](fakeContext)
val value = new AtomicInteger(0)
cache.get("foo", supplier(value.incrementAndGet()), fakeContext) mustEqual 1
}
"store different values for different keys" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = createInitializedCache[String, Int](fakeContext)
val value1 = new AtomicInteger(0)
cache.get("foo", supplier(value1.incrementAndGet()), fakeContext) mustEqual 1
val value2 = new AtomicInteger(100)
cache.get("bar", supplier(value2.incrementAndGet()), fakeContext) mustEqual 101
// Recheck to make sure the later calls to get had no effect on the previous ones
cache.get("foo", supplier(value1.incrementAndGet()), fakeContext) mustEqual 1
cache.get("bar", supplier(value2.incrementAndGet()), fakeContext) mustEqual 101
}
"store different values for different requests" >> {
val cache = new DedupingCache[String, Int]
val fakeContext1 = new Context(new RequestBuilder().id(123L))
val fakeContext2 = new Context(new RequestBuilder().id(456L))
cache.initCacheForRequest(fakeContext1)
cache.initCacheForRequest(fakeContext2)
val value1 = new AtomicInteger(0)
cache.get("foo", supplier(value1.incrementAndGet()), fakeContext1) mustEqual 1
val value2 = new AtomicInteger(100)
cache.get("foo", supplier(value2.incrementAndGet()), fakeContext2) mustEqual 101
// Recheck to make sure the later calls to get had no effect on the previous ones
cache.get("foo", supplier(value1.incrementAndGet()), fakeContext1) mustEqual 1
cache.get("foo", supplier(value2.incrementAndGet()), fakeContext2) mustEqual 101
}
"only call valueIfMissing once, no matter how many times we call get on the same key" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = createInitializedCache[String, Int](fakeContext)
val value = new AtomicInteger(0)
cache.get("foo", supplier(value.incrementAndGet()), fakeContext) mustEqual 1
cache.get("foo", supplier(value.incrementAndGet()), fakeContext) mustEqual 1
cache.get("foo", supplier(value.incrementAndGet()), fakeContext) mustEqual 1
}
"not call valueIfMissing at all if the key is already in the cache" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = createInitializedCache[String, Int](fakeContext)
val originalValue = new AtomicInteger(0)
cache.get("foo", supplier(originalValue.incrementAndGet()), fakeContext) mustEqual 1
val newValue = new AtomicInteger(100)
cache.get("foo", supplier(newValue.incrementAndGet()), fakeContext) mustEqual 1
}
"throw an exception if the cache has been cleaned up for the current request" >> {
val fakeContext = new Context(new RequestBuilder)
val cache = createInitializedCache[String, Int](fakeContext)
val value = new AtomicInteger(0)
cache.get("foo", supplier(value.incrementAndGet()), fakeContext) mustEqual 1
cache.cleanupCacheForRequest(fakeContext)
cache.get("foo", supplier(1), fakeContext) must throwA[CacheNotInitializedException]
}
}
private def createInitializedCache[K, V](context: Context): DedupingCache[K, V] = {
val cache = new DedupingCache[K, V]
cache.initCacheForRequest(context)
cache
}
private def supplier[A](value: => A): Supplier[A] = {
new Supplier[A] {
override def get(): A = value
}
}
}
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/bigpipe/TestBigPipeJavaScript.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import javax.script.{ScriptEngine, ScriptEngineManager}
import jdk.nashorn.api.scripting.JSObject
import org.specs2.mutable.Specification
import play.api.libs.json.Json
import scala.io.Source
class TestBigPipeJavaScript extends Specification {
private val BigPipeJsPath = "public/com/ybrikman/ping/big-pipe.js"
"big-pipe.js should" >> {
"be able to unescape HTML escaped by Embed.escapeForEmbedding" >> {
val html = "<h1>Hello World</h1><!-- The dashes in this comment should be escaped -->"
val escapedHtml = Embed.escapeForEmbedding(html).body
val engine = getJsEngineWithBigPipeLoaded
val result = engine.eval(s"""BigPipe.unescapeForEmbedding('$escapedHtml')""")
result.toString mustEqual html
}
"be able to unescape JSON escaped by Embed.escapeForEmbedding" >> {
val data = Map("foo" -> "bar", "baz--" -> "--blah--")
val json = Json.stringify(Json.toJson(data))
val escapedJson = Embed.escapeForEmbedding(json).body
val engine = getJsEngineWithBigPipeLoaded
val result = engine.eval(
s"""
|BigPipe.readEmbeddedContentFromDom = function(domId) {
| return '$escapedJson';
|};
|BigPipe.parseEmbeddedJsonFromDom('foo')""".stripMargin).asInstanceOf[JSObject]
result.getMember("foo") mustEqual data("foo")
result.getMember("baz--") mustEqual data("baz--")
}
"ignore null content when unescaping" >> {
val engine = getJsEngineWithBigPipeLoaded
engine.eval("""BigPipe.unescapeForEmbedding(null)""") must beNull
}
}
private def getJsEngineWithBigPipeLoaded: ScriptEngine = {
// Must create the ScriptEngineManager with the class loader or getEngineByName will return null in SBT
// See: https://github.com/sbt/sbt/issues/1214#issuecomment-55566056
val engine = new ScriptEngineManager(getClass.getClassLoader).getEngineByName("nashorn")
engine.eval(getBigPipeJs)
engine
}
private def getBigPipeJs: String = {
val inputStream = getClass.getClassLoader.getResourceAsStream(BigPipeJsPath)
Source.fromInputStream(inputStream).mkString
}
}
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/bigpipe/TestEmbed.scala
================================================
package com.ybrikman.ping.scalaapi.bigpipe
import org.specs2.mutable.Specification
import play.twirl.api.Html
class TestEmbed extends Specification {
"The Embed.escapeForEmbedding method should" >> {
"leave content without dashes unchanged" >> {
Embed.escapeForEmbedding("foo bar baz") mustEqual Html("foo bar baz")
}
"leave content with single dashes unchanged" >> {
Embed.escapeForEmbedding("foo-bar-baz") mustEqual Html("foo-bar-baz")
}
"escape all double dashes" >> {
Embed.escapeForEmbedding("foo--bar--baz") mustEqual Html("foo\\u002d\\u002dbar\\u002d\\u002dbaz")
}
}
}
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestBeforeAndAfterFilter.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import java.util.concurrent.CopyOnWriteArrayList
import org.specs2.mutable.Specification
import play.api.libs.iteratee.Enumerator
import play.api.mvc.{Result, RequestHeader, Results}
import play.api.test.FakeRequest
import play.api.test.Helpers._
import scala.collection.JavaConverters._
import play.api.libs.concurrent.Execution.Implicits._
import scala.concurrent.Future
class TestBeforeAndAfterFilter extends Specification {
"BeforeAndAfterFilter should" >> {
"Call the before function once before the rest of the filter chain and the after function once after the rest of the filter chain" >> {
val events = new CopyOnWriteArrayList[String]()
val expectedResult = "testing"
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.successful(Results.Ok(expectedResult))
}
val actualResult = contentAsString(filter(next _)(FakeRequest()))
actualResult mustEqual expectedResult
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain throws an exception" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
throw new BeforeAndAfterTestException
}
contentAsString(filter(next _)(FakeRequest())) must throwA[BeforeAndAfterTestException]
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain returns a failed future" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.failed(new BeforeAndAfterTestException)
}
contentAsString(filter(next _)(FakeRequest())) must throwA[BeforeAndAfterTestException]
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain returns an Enumerator that is already done" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.successful(Results.Ok.chunked(Enumerator.eof[String]))
}
contentAsString(filter(next _)(FakeRequest())) mustEqual ""
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain returns an Enumerator that is empty" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.successful(Results.Ok.chunked(Enumerator.empty[String]))
}
contentAsString(filter(next _)(FakeRequest())) mustEqual ""
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain returns an Enumerator that throws an exception" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.successful(Results.Ok.chunked(Enumerator.repeatM[String](throw new BeforeAndAfterTestException)))
}
contentAsString(filter(next _)(FakeRequest())) must throwA[BeforeAndAfterTestException]
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
"Call the before and after functions even if the next item in the filter chain returns an Enumerator built from a failed Future" >> {
val events = new CopyOnWriteArrayList[String]()
val filter = new BeforeAndAfterFilter(
before = rh => events.add("before"),
after = rh => events.add("after"))
def next(rh: RequestHeader): Future[Result] = {
events.add("next")
Future.successful(Results.Ok.chunked(Enumerator.repeatM[String](Future.failed(new BeforeAndAfterTestException))))
}
contentAsString(filter(next _)(FakeRequest())) must throwA[BeforeAndAfterTestException]
events.asScala must containTheSameElementsAs(Seq("before", "next", "after"))
}
}
}
class BeforeAndAfterTestException extends RuntimeException
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestCache.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import java.util.concurrent.atomic.AtomicInteger
import org.specs2.mutable.Specification
class TestCache extends Specification {
"The Cache get method should" >> {
"return None on an empty cache" >> {
val cache = new Cache[String, String]()
cache.get("foo") must beNone
}
"return a value that you insert" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
}
"return None if you insert one value but get a different one" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("baz") must beNone
}
"return a new value when you overwrite an old one" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
cache.put("foo", "baz") must beSome("bar")
cache.get("foo") must beSome("baz")
}
}
"The Cache contains method should" >> {
"return false for any key when the cache is empty" >> {
val cache = new Cache[String, String]()
cache.contains("foo") must beFalse
}
"return true when you lookup a key that was previously inserted" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.contains("foo") must beTrue
}
"return false when you lookup a different key than the one that was previously inserted" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.contains("baz") must beFalse
}
}
"Cache getOrElseUpdate method should" >> {
"return the new value on an empty cache" >> {
val cache = new Cache[String, String]()
cache.getOrElseUpdate("foo", "bar") mustEqual "bar"
cache.get("foo") must beSome("bar")
}
"return the old value if the same key had already been inserted and not overwrite the old value" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.getOrElseUpdate("foo", "baz") mustEqual "bar"
cache.get("foo") must beSome("bar")
}
"evaluate the value exactly once if the key was not already present in the cache" >> {
val cache = new Cache[String, LazyEvalTestClass]()
// No value previously present, make sure new value gets evaluated correctly
val evalCount = new AtomicInteger(0)
val result = cache.getOrElseUpdate("foo", LazyEvalTestClass(evalCount, failIfEvaluated = false, uniqueId = 5))
evalCount.get() mustEqual 1
result.uniqueId mustEqual 5
// Make sure fetching the value later doesn't cause it to be evaluated again
val getResult = cache.get("foo")
getResult.isDefined must beTrue
getResult.get.uniqueId mustEqual 5
evalCount.get() mustEqual 1
}
"not evaluate the value at all if the key was already present in the cache" >> {
val cache = new Cache[String, LazyEvalTestClass]()
val evalCountPrevious = new AtomicInteger(0)
cache.put("foo", LazyEvalTestClass(evalCountPrevious, failIfEvaluated = false, uniqueId = 5)) must beNone
evalCountPrevious.get() mustEqual 1
// Make sure inserting another value at the same key does not result in the new value being evaluated
val evalCountNew = new AtomicInteger(0)
val result = cache.getOrElseUpdate("foo", LazyEvalTestClass(evalCountNew, failIfEvaluated = true, uniqueId = 123))
evalCountNew.get() mustEqual 0
result.uniqueId mustEqual 5
// Make sure calling get has no effect on the new value either
val getResult = cache.get("foo")
getResult.isDefined must beTrue
evalCountNew.get() mustEqual 0
evalCountPrevious.get() mustEqual 1
getResult.get.uniqueId mustEqual 5
}
}
"Cache putIfAbsent method should" >> {
"insert the value if the key was not already in the cache" >> {
val cache = new Cache[String, String]()
cache.putIfAbsent("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
}
"not overwrite the previous value if it was already in the cache" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.putIfAbsent("foo", "baz") must beSome("bar")
cache.get("foo") must beSome("bar")
}
"insert the value if the cache contained values for other keys" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.putIfAbsent("bar", "baz") must beNone
cache.get("foo") must beSome("bar")
cache.get("bar") must beSome("baz")
}
}
"Cache remove method should" >> {
"return None on an empty cache" >> {
val cache = new Cache[String, String]()
cache.remove("foo") must beNone
}
"remove values that were inserted previously" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
cache.remove("foo") must beSome("bar")
cache.get("foo") must beNone
}
"only remove the requested keys" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
cache.put("baz", "blah") must beNone
cache.get("baz") must beSome("blah")
cache.remove("foo") must beSome("bar")
cache.get("foo") must beNone
cache.get("baz") must beSome("blah")
}
}
"Cache empty method should" >> {
"leave an empty cache empty" >> {
val cache = new Cache[String, String]()
cache.size mustEqual 0
cache.clear()
cache.size mustEqual 0
}
"remove all values from the cache" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.get("foo") must beSome("bar")
cache.put("baz", "blah") must beNone
cache.get("baz") must beSome("blah")
cache.clear()
cache.get("foo") must beNone
cache.get("baz") must beNone
}
}
"Cache size method should" >> {
"return 0 for an empty cache" >> {
val cache = new Cache[String, String]()
cache.size mustEqual 0
}
"return the number of elements inserted into the cache so far" >> {
val cache = new Cache[String, String]()
cache.size mustEqual 0
cache.put("foo", "bar") must beNone
cache.size mustEqual 1
cache.put("baz", "blah") must beNone
cache.size mustEqual 2
cache.put("baz", "abcdef") must beSome("blah")
cache.size mustEqual 2
}
}
"The Cache should" >> {
"behave correctly across many put, get, and remove calls" >> {
val cache = new Cache[Int, Int]()
val range = 0 until 1000
val rangeWrappedInOptions = range.map(Option.apply)
range.map(i => cache.put(i, i)) must contain(beNone).forall
range.map(i => cache.get(i)) must containTheSameElementsAs(range.map(Option.apply))
range.map(i => cache.remove(i)) must containTheSameElementsAs(range.map(Option.apply))
range.map(i => cache.get(i)) must contain(beNone).forall
}
"behave correctly across many getOrElseUpdate calls" >> {
val cache = new Cache[Int, LazyEvalTestClass]()
val smallRange = 0 until 500
// Insert some initial values and make sure they each get evaluated exactly once
val evalCountsAfterPut = smallRange.map { i =>
val evalCount = new AtomicInteger(0)
cache.put(i, LazyEvalTestClass(evalCount, failIfEvaluated = false, uniqueId = i))
evalCount.get()
}
evalCountsAfterPut must contain(1).forall
val bigRange = 0 until 1000
val constant = 123456
// Now use getOrElseUpdate to insert more values; the first half should already be in the cache and therefore
// skipped (so we expect their evalCount to be 0), while the second half should be new entries that get inserted
// (so we expect their evalCount to be 1)
val idsAndEvalCountsAfterGetOrElseUpdate = bigRange.map { i =>
val evalCount = new AtomicInteger(0)
val shouldBeEvaluated = !smallRange.contains(i)
val uniqueId = i + constant
val result = cache.getOrElseUpdate(i, LazyEvalTestClass(evalCount, !shouldBeEvaluated, uniqueId))
(result.uniqueId, evalCount.get())
}
val expectedIdsAndEvalCountsAfterGetOrElseUpdate = bigRange.map { i =>
val shouldBeEvaluated = !smallRange.contains(i)
val uniqueId = i + constant
val expectedId = if (shouldBeEvaluated) uniqueId else i
val expectedEvalCount = if (shouldBeEvaluated) 1 else 0
(expectedId, expectedEvalCount)
}
idsAndEvalCountsAfterGetOrElseUpdate must containTheSameElementsAs(expectedIdsAndEvalCountsAfterGetOrElseUpdate)
// One last sanity check: make sure all the values are in the cache and have a count of 1
val actualGetResults = bigRange.map(i => cache.get(i).map(_.evalCount.get()))
actualGetResults must containTheSameElementsAs(bigRange.map(i => Some(1)))
}
}
"Cache getAll method should" >> {
"return an empty Map for an empty cache" >> {
val cache = new Cache[String, String]()
cache.getAll mustEqual Map.empty
}
"return a Map with the values in the cache" >> {
val cache = new Cache[String, String]()
cache.put("foo", "bar") must beNone
cache.put("baz", "blah") must beNone
cache.getAll mustEqual Map("foo"->"bar", "baz" -> "blah")
}
}
}
/**
* Used to test lazy evaluation. This class increments the evalCount whenever the constructor is called. It also fails
* the test if failIfEvaluated was set to true and the constructor gets called. This can be used to fail a
* test if some lazy value was evaluated when it should not have been.
*
* @param evalCount
* @param failIfEvaluated
*/
case class LazyEvalTestClass(evalCount: AtomicInteger, failIfEvaluated: Boolean, uniqueId: Int) {
evalCount.incrementAndGet()
require(!failIfEvaluated)
// Need to override the equals method generated by the case class because the AtomicInteger class does not implement
// equals or hashCode: http://stackoverflow.com/questions/7567502/why-are-two-atomicintegers-never-equal
override def equals(obj: Any): Boolean = {
obj match {
case other: LazyEvalTestClass =>
evalCount.get() == other.evalCount.get() &&
failIfEvaluated == other.failIfEvaluated &&
uniqueId == other.uniqueId
case _ => false
}
}
}
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestCacheFilter.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import org.specs2.mutable.Specification
import play.api.libs.concurrent.Execution.Implicits._
import play.api.mvc.{Results, Result, RequestHeader}
import play.api.test.FakeRequest
import play.api.test.Helpers._
import scala.concurrent.Future
class TestCacheFilter extends Specification {
"The Scala CacheFilter should" >> {
"initialize the cache before the filter chain and clean it up after the filter chain" >> {
val cache = new DedupingCache[String, String]
val filter = new CacheFilter(cache)
val expectedResult = "bar"
def next(rh: RequestHeader): Future[Result] = {
Future.successful(Results.Ok(cache.get("foo", expectedResult)(rh)))
}
val fakeRequest = FakeRequest()
val actualResult = contentAsString(filter(next _)(fakeRequest))
actualResult mustEqual expectedResult
// Ensure cache was cleaned up for that request
cache.get("foo", "shouldNotBeUsed")(fakeRequest) must throwA[CacheNotInitializedException]
}
"initialize the cache before the filter chain and clean it up after the filter chain even if an exception is thrown" >> {
val cache = new DedupingCache[String, String]
val filter = new CacheFilter(cache)
val expectedResult = "bar"
def next(rh: RequestHeader): Future[Result] = {
throw new CacheFilterTestException(cache.get("foo", expectedResult)(rh))
}
val fakeRequest = FakeRequest()
contentAsString(filter(next _)(fakeRequest)) must throwA[CacheFilterTestException](message = expectedResult)
// Ensure cache was cleaned up for that request
cache.get("foo", "shouldNotBeUsed")(fakeRequest) must throwA[CacheNotInitializedException]
}
}
}
class CacheFilterTestException(message: String) extends RuntimeException(message)
================================================
FILE: big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestDedupingCache.scala
================================================
package com.ybrikman.ping.scalaapi.dedupe
import java.util.concurrent.atomic.AtomicInteger
import org.specs2.mutable.Specification
import play.api.mvc.RequestHeader
import play.api.test.FakeRequest
class TestDedupingCache extends Specification {
"The Scala DedupingCache get method should" >> {
"throw an exception if the cache is not initialized" >> {
val uninitializedCache = new DedupingCache[String, Integer]
uninitializedCache.get("foo", 1)(FakeRequest()) must throwA[CacheNotInitializedException]
}
"return valueIfMissing when the cache is empty" >> {
implicit val fakeRequest = FakeRequest()
val cache = createInitializedCache[String, Int]
val value = new AtomicInteger(0)
cache.get("foo", value.incrementAndGet()) mustEqual 1
}
"store different values for different keys" >> {
implicit val fakeRequest = FakeRequest()
val cache = createInitializedCache[String, Int]
val value1 = new AtomicInteger(0)
cache.get("foo", value1.incrementAndGet()) mustEqual 1
val value2 = new AtomicInteger(100)
cache.get("bar", value2.incrementAndGet()) mustEqual 101
// Recheck to make sure the later calls to get had no effect on the previous ones
cache.get("foo", value1.incrementAndGet()) mustEqual 1
cache.get("bar", value2.incrementAndGet()) mustEqual 101
}
"store different values for different requests" >> {
val cache = new DedupingCache[String, Int]
val fakeRequest1 = FakeRequest().copy(id = 123)
val fakeRequest2 = FakeRequest().copy(id = 456)
cache.initCacheForRequest(fakeRequest1)
cache.initCacheForRequest(fakeRequest2)
val value1 = new AtomicInteger(0)
cache.get("foo", value1.incrementAndGet())(fakeRequest1) mustEqual 1
val value2 = new AtomicInteger(100)
cache.get("foo", value2.incrementAndGet())(fakeRequest2) mustEqual 101
// Recheck to make sure the later calls to get had no effect on the previous ones
cache.get("foo", value1.incrementAndGet())(fakeRequest1) mustEqual 1
cache.get("foo", value2.incrementAndGet())(fakeRequest2) mustEqual 101
}
"only call valueIfMissing once, no matter how many times we call get on the same key" >> {
implicit val fakeRequest = FakeRequest()
val cache = createInitializedCache[String, Int]
val value = new AtomicInteger(0)
cache.get("foo", value.incrementAndGet()) mustEqual 1
cache.get("foo", value.incrementAndGet()) mustEqual 1
cache.get("foo", value.incrementAndGet()) mustEqual 1
}
"not call valueIfMissing at all if the key is already in the cache" >> {
implicit val fakeRequest = FakeRequest()
val cache = createInitializedCache[String, Int]
val originalValue = new AtomicInteger(0)
cache.get("foo", originalValue.incrementAndGet()) mustEqual 1
val newValue = new AtomicInteger(100)
cache.get("foo", newValue.incrementAndGet()) mustEqual 1
}
"throw an exception if the cache has been cleaned up for the current request" >> {
implicit val fakeRequest = FakeRequest()
val cache = createInitializedCache[String, Int]
val value = new AtomicInteger(0)
cache.get("foo", value.incrementAndGet()) mustEqual 1
cache.cleanupCacheForRequest(fakeRequest)
cache.get("foo", 1) must throwA[CacheNotInitializedException]
}
}
private def createInitializedCache[K, V](implicit request: RequestHeader): DedupingCache[K, V] = {
val cache = new DedupingCache[K, V]
cache.initCacheForRequest(request)
cache
}
}
================================================
FILE: build.sbt
================================================
import ReleaseTransformations._
// The BigPipe library
lazy val bigPipe = (project in file("big-pipe"))
.settings(bigPipeSettings:_*)
.enablePlugins(SbtTwirl)
// Some shared code for the sample apps
lazy val sampleAppCommon = (project in file("sample-app-common"))
.settings(sampleAppCommonSettings:_*)
.enablePlugins(SbtTwirl)
.dependsOn(bigPipe)
// The Scala sample app
lazy val sampleAppScala = (project in file("sample-app-scala"))
.settings(sampleAppScalaSettings:_*)
.enablePlugins(PlayScala)
.dependsOn(bigPipe, sampleAppCommon % "test->test;compile->compile")
// The Java sample app
lazy val sampleAppJava = (project in file("sample-app-java"))
.settings(sampleAppJavaSettings:_*)
.enablePlugins(PlayJava)
.dependsOn(bigPipe, sampleAppCommon % "test->test;compile->compile")
// The root project
lazy val root = (project in file("."))
.aggregate(bigPipe, sampleAppCommon, sampleAppScala, sampleAppJava)
.settings(rootSettings:_*)
// Settings shared by all the projects
lazy val commonSettings = Seq(
organization := "com.ybrikman.ping",
scalaVersion := "2.11.6",
scalacOptions += "-feature",
resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
) ++ publishSettings
// Settings specific to the bigPipe project
lazy val bigPipeSettings = Seq(
name := "big-pipe",
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play" % play.core.PlayVersion.current,
"com.typesafe.play" %% "play-iteratees" % play.core.PlayVersion.current,
specs2 % Test
)
) ++ commonSettings
// Settings specific to the sampleAppCommon project
lazy val sampleAppCommonSettings = Seq(
name := "sample-app-common",
libraryDependencies ++= Seq(
"com.typesafe.play" %% "play" % play.core.PlayVersion.current,
ws % Test,
specs2 % Test
)
) ++ commonSettings ++ streamingTemplateSettings
// Settings specific to the sampleAppScala project
lazy val sampleAppScalaSettings = Seq(
name := "sample-app-scala",
routesGenerator := InjectedRoutesGenerator,
libraryDependencies ++= Seq(
ws,
specs2 % Test
),
// These two settings are to ensure the test servers don't use the same port if they happen to run in parallel
fork in Test := true,
javaOptions in Test += "-Dtestserver.port=19111"
) ++ commonSettings ++ streamingTemplateSettings
// Settings specific to the sampleAppJava project
lazy val sampleAppJavaSettings = Seq(
name := "sample-app-java",
routesGenerator := InjectedRoutesGenerator,
libraryDependencies ++= Seq(
javaWs,
"com.typesafe.play" %% "play-java" % play.core.PlayVersion.current,
specs2 % Test
),
// These two settings are to ensure the test servers don't use the same port if they happen to run in parallel
fork in Test := true,
javaOptions in Test += "-Dtestserver.port=19222"
) ++ commonSettings ++ streamingTemplateSettings
// Settings specific to the root project
lazy val rootSettings = Seq(
updateVersionNumberInReadme := {
val ReadmeFile = "README.md"
val readmePath = baseDirectory.value / ReadmeFile
val readmeText = IO.read(readmePath)
val releaseVersion = version.value
streams.value.log.info(s"Updating version number in $readmePath to $releaseVersion")
val DependencyRegex = """("com.ybrikman.ping" %% "big-pipe" % ")(.+?)(")""".r
val updatedReadmeText = DependencyRegex.replaceAllIn(readmeText, "$1" + releaseVersion + "$3")
IO.write(readmePath, updatedReadmeText)
val vcs = releaseVcs.value.getOrElse(throw new RuntimeException("Could not find a version control system to commit README changes"))
vcs.add(ReadmeFile) !! streams.value.log
val status = (vcs.status !!).trim
if (status.nonEmpty) {
streams.value.log.info("Committing changes to $readmePath")
vcs.commit(s"Updating version number in $ReadmeFile to $releaseVersion") ! streams.value.log
}
releaseVersion
}
) ++ commonSettings
// You must added these settings to your Play app to be able to use .scala.stream templates for BigPipe-style streaming
lazy val streamingTemplateSettings = Seq(
TwirlKeys.templateFormats ++= Map("stream" -> "com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamFormat"),
TwirlKeys.templateImports ++= Vector("com.ybrikman.ping.scalaapi.bigpipe.HtmlStream", "com.ybrikman.ping.scalaapi.bigpipe._")
)
lazy val updateVersionNumberInReadme = taskKey[String]("Updates the version number in the README to the current version")
// Used to publish the bigPipe project to Sonatype as per http://www.scala-sbt.org/release/docs/Using-Sonatype.html
lazy val publishSettings = Seq(
publishMavenStyle := true,
sonatypeProfileName := "com.ybrikman",
pomIncludeRepository := { _ => false },
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
},
licenses := Seq("MIT License" -> url("http://www.opensource.org/licenses/mit-license.php")),
homepage := Some(url("https://github.com/brikis98/ping-play")),
scmInfo := Some(ScmInfo(url("https://github.com/brikis98/ping-play"), "scm:git:git@github.com:brikis98/ping-play.git")),
// The "developers" key does not get inserted into the POM correctly, so we have to use pomExtra and do it manually
// developers := List(Developer("brikis98", "Yevgeniy Brikman", "jim@ybrikman.com", url("http://www.ybrikman.com"))),
pomExtra := (
<developers>
<developer>
<id>brikis98</id>
<name>Yevgeniy Brikman</name>
<url>http://www.ybrikman.com</url>
</developer>
</developers>
),
releaseProcess := Seq[ReleaseStep](
checkSnapshotDependencies,
inquireVersions,
runClean,
runTest,
setReleaseVersion,
releaseStepTask(updateVersionNumberInReadme),
commitReleaseVersion,
tagRelease,
ReleaseStep(action = Command.process("publishSigned", _)),
setNextVersion,
commitNextVersion,
ReleaseStep(action = Command.process("sonatypeReleaseAll", _)),
pushChanges
)
)
================================================
FILE: circle.yml
================================================
machine:
services:
- docker
dependencies:
cache_directories:
- "~/docker-compose-1.2.0"
- "~/docker-build-cache"
pre:
- if [[ ! -e ~/docker-compose-1.2.0 ]]; then mkdir -p ~/docker-compose-1.2.0 && curl -L https://github.com/docker/compose/releases/download/1.2.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose-1.2.0/docker-compose && chmod +x ~/docker-compose-1.2.0/docker-compose; fi
override:
- if [[ -e ~/docker-build-cache/brikis98-ping-play-image.tar ]]; then docker load -i ~/docker-build-cache/brikis98-ping-play-image.tar; fi
- docker build -t brikis98/ping-play .
- mkdir -p ~/docker-build-cache && docker save brikis98/ping-play > ~/docker-build-cache/brikis98-ping-play-image.tar
test:
override:
- ~/docker-compose-1.2.0/docker-compose run web activator test
================================================
FILE: docker-compose.yml
================================================
web:
image: brikis98/ping-play
volumes:
- .:/src
ports:
- "9000:9000"
stdin_open: true
================================================
FILE: project/build.properties
================================================
sbt.version=0.13.8
================================================
FILE: project/plugins.sbt
================================================
logLevel := Level.Warn
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1")
addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.0")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.5.0")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
================================================
FILE: sample-app-common/src/main/resources/public/javascripts/big-pipe-with-mustache.js
================================================
(function(window, mustache, BigPipe) {
"use strict";
var document = window.document;
var console = window.console;
// In a real app, you'd probably want to store the template in an external file and not inline it like this.
var template =
'<div class="module">' +
'<h3 class="id">{{ id }}</h3>' +
'<h6>took</h6>' +
'<h2 class="highlight">{{ delay }} ms</h2>' +
'<h6>to respond</h6>' +
'</div>';
// Override the original BigPipe.renderPagelet method with one that uses mustache.js for client-side rendering
BigPipe.renderPagelet = function(id, json) {
var domElement = document.getElementById(id);
if (domElement) {
domElement.innerHTML = Mustache.render(template, json);
} else {
console.log("ERROR: cannot render pagelet because DOM node with id " + id + " does not exist");
}
};
})(window, Mustache, BigPipe);
================================================
FILE: sample-app-common/src/main/resources/public/javascripts/mustache.js
================================================
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false Mustache: true*/
(function defineMustache (global, factory) {
if (typeof exports === 'object' && exports) {
factory(exports); // CommonJS
} else if (typeof define === 'function' && define.amd) {
define(['exports'], factory); // AMD
} else {
global.Mustache = {};
factory(Mustache); // script, wsh, asp
}
}(this, function mustacheFactory (mustache) {
var objectToString = Object.prototype.toString;
var isArray = Array.isArray || function isArrayPolyfill (object) {
return objectToString.call(object) === '[object Array]';
};
function isFunction (object) {
return typeof object === 'function';
}
function escapeRegExp (string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
}
/**
* Null safe way of checking whether or not an object,
* including its prototype, has a given property
*/
function hasProperty (obj, propName) {
return obj != null && typeof obj === 'object' && (propName in obj);
}
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
// See https://github.com/janl/mustache.js/issues/189
var regExpTest = RegExp.prototype.test;
function testRegExp (re, string) {
return regExpTest.call(re, string);
}
var nonSpaceRe = /\S/;
function isWhitespace (string) {
return !testRegExp(nonSpaceRe, string);
}
var entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
function escapeHtml (string) {
return String(string).replace(/[&<>"'\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
var whiteRe = /\s*/;
var spaceRe = /\s+/;
var equalsRe = /\s*=/;
var curlyRe = /\s*\}/;
var tagRe = /#|\^|\/|>|\{|&|=|!/;
/**
* Breaks up the given `template` string into a tree of tokens. If the `tags`
* argument is given here it must be an array with two string values: the
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
* course, the default is to use mustaches (i.e. mustache.tags).
*
* A token is an array with at least 4 elements. The first element is the
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
* all text that appears outside a symbol this element is "text".
*
* The second element of a token is its "value". For mustache tags this is
* whatever else was inside the tag besides the opening symbol. For text tokens
* this is the text itself.
*
* The third and fourth elements of the token are the start and end indices,
* respectively, of the token in the original template.
*
* Tokens that are the root node of a subtree contain two more elements: 1) an
* array of tokens in the subtree and 2) the index in the original template at
* which the closing tag for that section begins.
*/
function parseTemplate (template, tags) {
if (!template)
return [];
var sections = []; // Stack to hold section tokens
var tokens = []; // Buffer to hold the tokens
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var nonSpace = false; // Is there a non-space char on the current line?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
function stripSpace () {
if (hasTag && !nonSpace) {
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
function compileTags (tagsToCompile) {
if (typeof tagsToCompile === 'string')
tagsToCompile = tagsToCompile.split(spaceRe, 2);
if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
throw new Error('Invalid tags: ' + tagsToCompile);
openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
}
compileTags(tags || mustache.tags);
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
while (!scanner.eos()) {
start = scanner.pos;
// Match any text between tags.
value = scanner.scanUntil(openingTagRe);
if (value) {
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
nonSpace = true;
}
tokens.push([ 'text', chr, start, start + 1 ]);
start += 1;
// Check for whitespace on the current line.
if (chr === '\n')
stripSpace();
}
}
// Match the opening tag.
if (!scanner.scan(openingTagRe))
break;
hasTag = true;
// Get the tag type.
type = scanner.scan(tagRe) || 'name';
scanner.scan(whiteRe);
// Get the tag value.
if (type === '=') {
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') {
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
type = '&';
} else {
value = scanner.scanUntil(closingTagRe);
}
// Match the closing tag.
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
token = [ type, value, start, scanner.pos ];
tokens.push(token);
if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// Check section nesting.
openSection = sections.pop();
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
nonSpace = true;
} else if (type === '=') {
// Set the tags for the next time around.
compileTags(value);
}
}
// Make sure there are no open sections when we're done.
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* Combines the values of consecutive text tokens in the given `tokens` array
* to a single token.
*/
function squashTokens (tokens) {
var squashedTokens = [];
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* Forms the given array of `tokens` into a nested tree structure where
* tokens that represent a section have two additional items: 1) an array of
* all tokens that appear in that section and 2) the index in the original
* template that represents the end of that section.
*/
function nestTokens (tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[4] = [];
break;
case '/':
section = sections.pop();
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
/**
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
*/
function Scanner (string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
Scanner.prototype.eos = function eos () {
return this.tail === '';
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0)
return '';
var string = match[0];
this.tail = this.tail.substring(string.length);
this.pos += string.length;
return string;
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
Scanner.prototype.scanUntil = function scanUntil (re) {
var index = this.tail.search(re), match;
switch (index) {
case -1:
match = this.tail;
this.tail = '';
break;
case 0:
match = '';
break;
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
this.pos += match.length;
return match;
};
/**
* Represents a rendering context by wrapping a view object and
* maintaining a reference to the parent context.
*/
function Context (view, parentContext) {
this.view = view;
this.cache = { '.': this.view };
this.parent = parentContext;
}
/**
* Creates a new context using the given view with this context
* as the parent.
*/
Context.prototype.push = function push (view) {
return new Context(view, this);
};
/**
* Returns the value of the given name in this context, traversing
* up the context hierarchy if the value is absent in this context's view.
*/
Context.prototype.lookup = function lookup (name) {
var cache = this.cache;
var value;
if (cache.hasOwnProperty(name)) {
value = cache[name];
} else {
var context = this, names, index, lookupHit = false;
while (context) {
if (name.indexOf('.') > 0) {
value = context.view;
names = name.split('.');
index = 0;
/**
* Using the dot notion path in `name`, we descend through the
* nested objects.
*
* To be certain that the lookup has been successful, we have to
* check if the last object in the path actually has the property
* we are looking for. We store the result in `lookupHit`.
*
* This is specially necessary for when the value has been set to
* `undefined` and we want to avoid looking up parent contexts.
**/
while (value != null && index < names.length) {
if (index === names.length - 1)
lookupHit = hasProperty(value, names[index]);
value = value[names[index++]];
}
} else {
value = context.view[name];
lookupHit = hasProperty(context.view, name);
}
if (lookupHit)
break;
context = context.parent;
}
cache[name] = value;
}
if (isFunction(value))
value = value.call(this.view);
return value;
};
/**
* A Writer knows how to take a stream of tokens and render them to a
* string, given a context. It also maintains a cache of templates to
* avoid the need to parse the same template twice.
*/
function Writer () {
this.cache = {};
}
/**
* Clears all cached templates in this writer.
*/
Writer.prototype.clearCache = function clearCache () {
this.cache = {};
};
/**
* Parses and caches the given `template` and returns the array of tokens
* that is generated from the parse.
*/
Writer.prototype.parse = function parse (template, tags) {
var cache = this.cache;
var tokens = cache[template];
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* High-level method that is used to render the given `template` with
* the given `view`.
*
* The optional `partials` argument may be an object that contains the
* names and templates of partials that are used in the template. It may
* also be a function that is used to load partial templates on the fly
* that takes a single argument: the name of the partial.
*/
Writer.prototype.render = function render (template, view, partials) {
var tokens = this.parse(template);
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* Low-level method that renders the given array of `tokens` using
* the given `context` and `partials`.
*
* Note: The `originalTemplate` is only ever used to extract the portion
* of the original template that was contained in a higher-order section.
* If the template doesn't use higher-order sections, this argument may
* be omitted.
*/
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) {
var buffer = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token[0];
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate);
else if (symbol === '&') value = this.unescapedValue(token, context);
else if (symbol === 'name') value = this.escapedValue(token, context);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined)
buffer += value;
}
return buffer;
};
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
var self = this;
var buffer = '';
var value = context.lookup(token[1]);
// This function is used to render an arbitrary template
// in the current context by higher-order sections.
function subRender (template) {
return self.render(template, context, partials);
}
if (!value) return;
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
}
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
} else if (isFunction(value)) {
if (typeof originalTemplate !== 'string')
throw new Error('Cannot use higher-order sections without the original template');
// Extract the portion of the original template that the section contains.
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
return buffer;
};
Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
var value = context.lookup(token[1]);
// Use JavaScript's definition of falsy. Include empty arrays.
// See https://github.com/janl/mustache.js/issues/186
if (!value || (isArray(value) && value.length === 0))
return this.renderTokens(token[4], context, partials, originalTemplate);
};
Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
if (!partials) return;
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
return this.renderTokens(this.parse(value), context, partials, value);
};
Writer.prototype.unescapedValue = function unescapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return value;
};
Writer.prototype.escapedValue = function escapedValue (token, context) {
var value = context.lookup(token[1]);
if (value != null)
return mustache.escape(value);
};
Writer.prototype.rawValue = function rawValue (token) {
return token[1];
};
mustache.name = 'mustache.js';
mustache.version = '2.1.2';
mustache.tags = [ '{{', '}}' ];
// All high-level mustache.* functions use this writer.
var defaultWriter = new Writer();
/**
* Clears all cached templates in the default writer.
*/
mustache.clearCache = function clearCache () {
return defaultWriter.clearCache();
};
/**
* Parses and caches the given template in the default writer and returns the
* array of tokens it contains. Doing this ahead of time avoids the need to
* parse templates on the fly as they are rendered.
*/
mustache.parse = function parse (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* Renders the `template` with the given `view` and `partials` using the
* default writer.
*/
mustache.render = function render (template, view, partials) {
return defaultWriter.render(template, view, partials);
};
// This is here for backwards compatibility with 0.4.x.,
/*eslint-disable */ // eslint wants camel cased function name
mustache.to_html = function to_html (template, view, partials, send) {
/*eslint-enable*/
var result = mustache.render(template, view, partials);
if (isFunction(send)) {
send(result);
} else {
return result;
}
};
// Export the escaping function so that the user may override it.
// See https://github.com/janl/mustache.js/issues/244
mustache.escape = escapeHtml;
// Export these mainly for testing, but also for advanced usage.
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
}));
================================================
FILE: sample-app-common/src/main/resources/public/javascripts/timing.js
================================================
// Quick, hacky code used to display page load timing using the Navigation Timing API
(function(window) {
"use strict";
var document = window.document;
if (window.performance && window.performance.timing) {
var timing = window.performance.timing;
var timeToFirstByte = timing.responseStart - timing.requestStart;
var timeToDomLoading = timing.domLoading - timing.requestStart;
document.getElementById("time-to-first-byte").innerHTML = timeToFirstByte + "ms";
document.getElementById("time-to-dom-loading").innerHTML = timeToDomLoading + "ms";
} else {
document.getElementById("time-to-first-byte").innerHTML = "Navigation Timing API not supported in this browser"
document.getElementById("time-to-dom-loading").innerHTML = "Navigation Timing API not supported in this browser"
}
})(window);
================================================
FILE: sample-app-common/src/main/resources/public/stylesheets/main.css
================================================
body {
padding: 20px;
}
.wrapper td {
width: 200px;
height: 160px;
border: 1px solid #CCC;
border-radius: 5px;
text-align: center;
margin: 20px;
}
#timing {
margin: 20px 0;
}
#timing td {
padding-right: 10px;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Helvetica Neue", Helvetica, Arial;
margin: 0;
}
h1 {
font-size: 48px;
}
h2 {
font-size: 36px;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 12px;
}
.highlight {
color: #0077b5;
}
================================================
FILE: sample-app-common/src/main/scala/data/FakeServiceClient.scala
================================================
package data
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.json.{Json, JsValue}
import scala.concurrent.Future
import scala.util.Random
import data.Response._
/**
* A client that represents fake calls to remote backend services.
*/
class FakeServiceClient(futureUtil: FutureUtil) {
import data.FakeServiceClient._
def fakeRemoteCallFast(id: String): Future[Response] = fakeRemoteCall(id, FAST_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallMedium(id: String): Future[Response] = fakeRemoteCall(id, MEDIUM_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallSlow(id: String): Future[Response] = fakeRemoteCall(id, SLOW_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallJsonFast(id: String): Future[JsValue] = fakeRemoteCallJson(id, FAST_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallJsonMedium(id: String): Future[JsValue] = fakeRemoteCallJson(id, MEDIUM_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallJsonSlow(id: String): Future[JsValue] = fakeRemoteCallJson(id, SLOW_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallJson(id: String, delayInMillis: Long): Future[JsValue] = fakeRemoteCall(id, delayInMillis).map(Json.toJson(_))
def fakeRemoteCallErrorFast(id: String): Future[Response] = fakeRemoteCallError(id, FAST_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallErrorMedium(id: String): Future[Response] = fakeRemoteCallError(id, MEDIUM_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallErrorSlow(id: String): Future[Response] = fakeRemoteCallError(id, SLOW_RESPONSE_TIME_IN_MILLIS)
def fakeRemoteCallError(id: String, delayInMillis: Long): Future[Response] = {
fakeRemoteCall(id, delayInMillis).map(response => throw FakeRemoteCallException(response))
}
def fakeRemoteCall(id: String, delayInMillis: Long): Future[Response] = {
val randomJitter = new Random().nextInt(delayInMillis.toInt).toLong
val delay = delayInMillis + randomJitter
val fakeJsonResponse = Response(id, delay)
futureUtil.timeout(fakeJsonResponse, delay)
}
}
object FakeServiceClient {
val FAST_RESPONSE_TIME_IN_MILLIS = 5
val MEDIUM_RESPONSE_TIME_IN_MILLIS = 500
val SLOW_RESPONSE_TIME_IN_MILLIS = 3000
val RESPONSE_TO_TEST_ESCAPING = "Escaping test <!-- This comment should be escaped in the HTML -->"
}
case class FakeRemoteCallException(response: Response) extends RuntimeException(s"""Error in "${response.id}" after ${response.delay} ms""")
================================================
FILE: sample-app-common/src/main/scala/data/FutureUtil.scala
================================================
package data
import java.util.concurrent.TimeUnit
import java.util.function.Supplier
import akka.actor.ActorSystem
import play.libs.F.Promise
import play.libs.HttpExecution
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration.FiniteDuration
import akka.pattern.after
class FutureUtil(actorSystem: ActorSystem) {
/**
* Return a Scala Future that will be redeemed with the given message after the specified delay.
*
* @param message
* @param delay
* @param unit
* @param ec
* @tparam A
* @return
*/
def timeout[A](message: => A, delay: Long, unit: TimeUnit = TimeUnit.MILLISECONDS)(implicit ec: ExecutionContext): Future[A] = {
after(FiniteDuration(delay, TimeUnit.MILLISECONDS), actorSystem.scheduler)(Future(message))
}
/**
* Return a Java Promise that will be redeemed with the given message after the specified delay.
*
* @param message
* @param delay
* @param unit
* @tparam A
* @return
*/
def timeout[A](message: Supplier[A], delay: Long, unit: TimeUnit): Promise[A] = {
timeout(message, delay, unit, HttpExecution.defaultContext())
}
/**
* Return a Java Promise that will be redeemed with the given message after the specified delay.
*
* @param message
* @param delay
* @param unit
* @param ec
* @tparam A
* @return
*/
def timeout[A](message: Supplier[A], delay: Long, unit: TimeUnit, ec: ExecutionContext): Promise[A] = {
Promise.wrap(timeout(message.get(), delay)(ec))
}
}
================================================
FILE: sample-app-common/src/main/scala/data/Response.scala
================================================
package data
import play.api.libs.json.Json
/**
* Simple class used to represent a response from a remote service
*
* @param id
* @param delay
*/
case class Response(id: String, delay: Long)
object Response {
implicit val responseWrites = Json.writes[Response]
}
================================================
FILE: sample-app-common/src/main/scala/data/UrlAndId.scala
================================================
package data
/**
* Silly container class to pass around the URL and ID
*
* @param url
* @param id
*/
case class UrlAndId(url: String, id: String)
================================================
FILE: sample-app-common/src/main/twirl/views/clientSideTemplating.scala.stream
================================================
@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
<!-- You need to include the BigPipe JavaScript at the top of the page -->
<script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
<!-- Include mustache.js, a client-side templating library -->
<script src="/assets/javascripts/mustache.js"></script>
<!-- Include custom code that will allow you to use BigPipe with mustache.js -->
<script src="/assets/javascripts/big-pipe-with-mustache.js"></script>
</head>
<body>
<h1>With Big Pipe and Client-Side Templating</h1>
@HtmlStream.fromHtml(views.html.helpers.timing())
@bigPipe.render { pagelets =>
<table class="wrapper">
<tr>
<td>@pagelets(profile.id)</td>
<td>@pagelets(ads.id)</td>
<td>@pagelets(feed.id)</td>
</tr>
<tr>
<td>@pagelets(search.id)</td>
<td>@pagelets(inbox.id)</td>
<td>@pagelets(graph.id)</td>
</tr>
</table>
}
</body>
</html>
================================================
FILE: sample-app-common/src/main/twirl/views/dedupe.scala.html
================================================
@(result1: data.UrlAndId, result2: data.UrlAndId, result3: data.UrlAndId, result4: data.UrlAndId)
<html>
<head>
<style>
td { border: 1px solid #CCC; padding: 10px 20px; }
.url { width: 50%; }
.id { width: 50%; text-align: center; }
</style>
</head>
<body>
<h1>Deduping example</h1>
<p>
Requests to the same remote URL should be de-duped, so only one request is actually sent, and all the others get
the same cached response.
</p>
<table>
<thead>
<tr>
<th>Service URL</th>
<th>Request ID</th>
</tr>
</thead>
<tbody>
<tr>
<td class="url">@result1.url</td>
<td class="id">@result1.id</td>
</tr>
<tr>
<td class="url">@result2.url</td>
<td class="id">@result2.id</td>
</tr>
<tr>
<td class="url">@result3.url</td>
<td class="id">@result3.id</td>
</tr>
<tr>
<td class="url">@result4.url</td>
<td class="id">@result4.id</td>
</tr>
</tbody>
</table>
</body>
</html>
================================================
FILE: sample-app-common/src/main/twirl/views/escaping.scala.stream
================================================
@(bigPipe: BigPipe, shouldBeEscaped: Pagelet)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
<!-- You need to include the BigPipe JavaScript at the top of the page -->
<script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
<!-- Include mustache.js, a client-side templating library -->
<script src="/assets/javascripts/mustache.js"></script>
<!-- Include custom code that will allow you to use BigPipe with mustache.js -->
<script src="/assets/javascripts/big-pipe-with-mustache.js"></script>
</head>
<body>
<h1>Big Pipe Escaping Test</h1>
@bigPipe.render { pagelets =>
@pagelets(shouldBeEscaped.id)
}
</body>
</html>
================================================
FILE: sample-app-common/src/main/twirl/views/helpers/error.scala.html
================================================
@(error: Throwable)
<div class="module error">
<h5>There was an</h5>
<h2 class="highlight id">error</h2>
<h5>fetching data for this pagelet</h5>
<h6>@error</h6>
</div>
================================================
FILE: sample-app-common/src/main/twirl/views/helpers/module.scala.html
================================================
@(response: data.Response)
<div class="module">
<h3 class="id">@response.id</h3>
<h6>took</h6>
<h2 class="highlight">@response.delay ms</h2>
<h6>to respond</h6>
</div>
================================================
FILE: sample-app-common/src/main/twirl/views/helpers/timing.scala.html
================================================
<!-- This code is included only to display timings -->
<table id="timing">
<tr>
<td><h4>Time to first byte:</h4></td>
<td><h3 id="time-to-first-byte" class="highlight"></h3></td>
</tr>
<tr>
<td><h4>Time to DOM loading:</h4></td>
<td><h3 id="time-to-dom-loading" class="highlight"></h3></td>
</tr>
</table>
<script src="/assets/javascripts/timing.js"></script>
================================================
FILE: sample-app-common/src/main/twirl/views/withBigPipe.scala.stream
================================================
@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
<!-- You need to include the BigPipe JavaScript at the top of the page -->
<script src="/assets/com/ybrikman/ping/big-pipe.js"></script>
</head>
<body>
<h1>With Big Pipe</h1>
@HtmlStream.fromHtml(views.html.helpers.timing())
<!--
Wrap the entire body of your page with a bigPipe.render call. The pagelets parameter contains a Map from
Pagelet id to the HtmlStream for that Pagelet. You should put the HtmlStream for each of your Pagelets
into the appropriate place in the markup.
-->
@bigPipe.render { pagelets =>
<table class="wrapper">
<tr>
<td>@pagelets(profile.id)</td>
<td>@pagelets(ads.id)</td>
<td>@pagelets(feed.id)</td>
</tr>
<tr>
<td>@pagelets(search.id)</td>
<td>@pagelets(inbox.id)</td>
<td>@pagelets(graph.id)</td>
</tr>
</table>
}
</body>
</html>
================================================
FILE: sample-app-common/src/main/twirl/views/withoutBigPipe.scala.html
================================================
@(profile: data.Response, graph: data.Response, feed: data.Response, inbox: data.Response, ads: data.Response, search: data.Response)
<html>
<head>
<link rel="stylesheet" href="/assets/stylesheets/main.css">
</head>
<body>
<h1>Without Big Pipe</h1>
@views.html.helpers.timing()
<table class="wrapper">
<tr>
<td><div id="profile">@views.html.helpers.module(profile)</div></td>
<td><div id="ads">@views.html.helpers.module(ads)</div></td>
<td><div id="feed">@views.html.helpers.module(feed)</div></td>
</tr>
<tr>
<td><div id="search">@views.html.helpers.module(search)</div></td>
<td><div id="inbox">@views.html.helpers.module(inbox)</div></td>
<td><div id="graph">@views.html.helpers.module(graph)</div></td>
</tr>
</table>
</body>
</html>
================================================
FILE: sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeSpec.scala
================================================
package com.ybrikman.ping
import play.api.test.WithBrowser
import data.FakeServiceClient
/**
* End-to-end tests of BigPipe functionality. The Scala and Java sample apps can extend this trait to run all the tests
* in it.
*/
trait BaseBigPipeSpec extends PingSpecification {
"The sample app" should {
"render the page without BigPipe" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/withoutBigPipe")
browser.$("#profile .id").getTexts.get(0) must equalTo("profile")
}
"render the page client-side with BigPipe" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/withBigPipe")
browser.$("#profile .id").getTexts.get(0) must equalTo("profile")
}
"render the page server-side with BigPipe" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/serverSideRendering")
browser.$("#profile .id").getTexts.get(0) must equalTo("profile")
}
"render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/clientSideTemplating")
browser.$("#profile .id").getTexts.get(0) must equalTo("profile")
}
"handle errors while rendering with BigPipe" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/errorHandling")
browser.$("#profile .id").getTexts.get(0) must equalTo("profile")
browser.$("#feed .id").getTexts.get(0) must equalTo("error")
}
"escape the body of pagelets when using BigPipe" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/escaping")
browser.$("#shouldBeEscaped .id").getTexts.get(0) must equalTo(FakeServiceClient.RESPONSE_TO_TEST_ESCAPING)
}
}
}
================================================
FILE: sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala
================================================
package com.ybrikman.ping
import com.ybrikman.ping.TimingHelper._
import com.ybrikman.ping.CustomRoutes._
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions
import com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamImplicits._
import com.ybrikman.ping.scalaapi.bigpipe._
import data.FutureUtil
import play.api.libs.concurrent.Execution.Implicits._
import play.api.libs.iteratee.{Enumerator, Iteratee}
import play.api.libs.ws.WSClient
import play.api.mvc.{Action, Results}
import play.api.routing.Router
import play.api.routing.sird._
import scala.concurrent.{ExecutionContext, Future}
/**
* Tests that BigPipe is actually streaming data as soon as it's available and that chunks are not blocked anywhere.
* The Scala and Java sample apps can extend this trait to run all the tests in it.
*/
trait BaseBigPipeTimingSpec extends PingSpecification {
"BigPipe streaming" should {
"Send down the data in-order, only after all of it is available, without BigPipe" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) {
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withoutBigPipe")
chunkTimings must not be empty
// Time-to-first-byte: make sure the first chunk was sent back after the maxDelay (within a tolerance of
// ToleranceInMillis)
val firstChunk = chunkTimings(0)
firstChunk.content mustEqual FirstChunkContent
firstChunk.timeElapsed must beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis)
// Make sure the contents for each pagelet were sent back exactly once
val pageletContentTiming = PageletIndices.flatMap { index =>
chunkTimings.filter(_.content.contains(content(index)))
}
pageletContentTiming must have size PageletIndices.size
// Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of
// ToleranceInMillis)
val expectedTimingMatchers = PageletIndices.map { index =>
beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis)
}
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder
}
"Send down the data out-of-order, as soon as any of it is available, with client-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) {
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeClientSide")
chunkTimings must not be empty
// Time-to-first-byte: make sure the first chunk was sent back almost immediately
val firstChunk = chunkTimings(0)
firstChunk.content mustEqual FirstChunkContent
firstChunk.timeElapsed must beLessThan(ToleranceInMillis)
// Placeholders: make sure all the placeholders were sent back almost immediately and exactly once
val pageletPlaceholderTiming = PageletIndices.flatMap { index =>
chunkTimings.filter(_.content.contains(placeholder(id(index))))
}
pageletPlaceholderTiming must have size PageletIndices.size
pageletPlaceholderTiming.map(_.timeElapsed) must contain(beLessThan(ToleranceInMillis)).forall
// Make sure the contents for each pagelet were sent back exactly once
val pageletContentTiming = PageletIndices.flatMap { index =>
chunkTimings.filter(_.content.contains(content(index)))
}
pageletContentTiming must have size PageletIndices.size
// Check that contents for each pagelet were delayed by no more and no less than
// DELAY_MULTIPLIER_IN_MILLIS (within a tolerance of ToleranceInMillis)
val expectedTimingMatchers = PageletIndices.map { index =>
val expecteDelay = delay(index)
beGreaterThan(expecteDelay - ToleranceInMillis) and beLessThan(expecteDelay + ToleranceInMillis)
}
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder
}
"Send down the data in-order, as soon as it's available, with server-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) {
val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeServerSide")
chunkTimings must not be empty
// Time-to-first-byte: make sure the first chunk was sent back almost immediately
val firstChunk = chunkTimings(0)
firstChunk.content mustEqual FirstChunkContent
firstChunk.timeElapsed must beLessThan(ToleranceInMillis)
// Make sure the contents for each pagelet were sent back exactly once
val pageletContentTiming = PageletIndices.flatMap { index =>
chunkTimings.filter(_.content.contains(content(index)))
}
pageletContentTiming must have size PageletIndices.size
// Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of
// ToleranceInMillis)
val expectedTimingMatchers = PageletIndices.map { index =>
beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis)
}
pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder
}
}
private def getTimings(wsClient: WSClient, url: String): Seq[Timing] = {
val initialTimings = Timings()
val (_, bodyEnumerator) = await(wsClient.url(url).getStream())
val checkTiming = Iteratee.fold[Array[Byte], Timings](initialTimings) { (timings, chunk) =>
timings.addChunk(chunk)
}
val timings = await(bodyEnumerator.run(checkTiming))
// Useful for debugging
println(s"Timings and content for url $url:\n")
timings.chunkTimings.foreach(timing => println(s"----- ${timing.timeElapsed} ms -----\n\n${timing.content}\n\n"))
timings.chunkTimings
}
}
object TimingHelper {
val PageletIndices = 1 until 5
// Each pagelet id will have this prefix to make it easier to find in the stream of data
val IdPrefix = "pagelet_id_"
// The contents of each pagelet will have this prefix to make it easier to find in the stream of data
val ContentPrefix = "pagelet_content_"
// The placeholder for each pagelet will have this prefix to make it easier to find in the stream of data
val PlaceHolderPrefix = "pagelet_placeholder_"
// The contents of the very first chunk that should be sent back by each page
val FirstChunkContent = "first_chunk_content"
// Each pagelet will be delayed by this many milliseconds
val DelayMultiplierInMillis = 3000L
// With tests running in parallel, things may get a bit delayed, so check all timings within this tolerance
val ToleranceInMillis = DelayMultiplierInMillis / 10
def id(index: Int): String = {
s"$IdPrefix$index"
}
def content(index: Int): String = {
s"$ContentPrefix$index"
}
def delay(index: Int): Long = {
(PageletIndices.size - index) * DelayMultiplierInMillis
}
def maxDelay: Long = {
delay(PageletIndices.head)
}
def placeholder(id: String): String = {
s"$PlaceHolderPrefix$id"
}
}
case class Timings(startTime: Long = System.currentTimeMillis(), chunkTimings: Seq[Timing] = Seq.empty) {
def addChunk(contents: Array[Byte]): Timings = {
val timeElapsed = System.currentTimeMillis() - startTime
copy(chunkTimings = chunkTimings :+ Timing(new String(contents, "UTF-8"), timeElapsed))
}
}
case class Timing(content: String, timeElapsed: Long)
class MockTextPagelet(id: String, content: Future[String]) extends TextPagelet(id, content) {
override def renderPlaceholder(implicit ec: ExecutionContext): HtmlStream = {
HtmlStream.fromHtml(com.ybrikman.bigpipe.html.pageletServerSide(placeholder(id), PageletConstants.EmptyContent))
}
}
object CustomRoutes {
def withRouterToTestTimings: Option[RouterComponents => Router] = {
def createRoutes(routerComponents: RouterComponents): Router = {
val futureUtil = new FutureUtil(routerComponents.actorSystem)
Router.from {
case GET(p"/withoutBigPipe") => Action.async {
val futures = mockRemoteServiceCalls(futureUtil).map(_._2)
Future.sequence(futures).map { contents =>
Results.Ok.chunked(Enumerator(FirstChunkContent).andThen(Enumerator(contents:_*)))
}
}
case GET(p"/withBigPipeClientSide") => Action {
val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) }
Results.Ok.chunked(renderPagelets(PageletRenderOptions.ClientSide, pagelets))
}
case GET(p"/withBigPipeServerSide") => Action {
val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) }
Results.Ok.chunked(renderPagelets(PageletRenderOptions.ServerSide, pagelets))
}
case GET(p"/warmup") => Action {
Results.Ok("warmup")
}
}
}
Option(createRoutes _)
}
// Generate a series of Futures that represent remote calls. The Futures are returned in reverse order, from slowest
// to fastest, as a way to demonstrate the advantages of out-of-order client-side rendering.
private def mockRemoteServiceCalls(futureUtil: FutureUtil): Seq[(String, Future[String])] = {
PageletIndices.map { index =>
id(index) -> futureUtil.timeout(content(index), delay(index))
}
}
private def renderPagelets(renderOptions: PageletRenderOptions, pagelets: Seq[Pagelet]): HtmlStream = {
val bigPipe = new BigPipe(renderOptions, pagelets:_*)
bigPipe.render { renderedPagelets =>
pagelets.foldLeft(HtmlStream.fromString(FirstChunkContent)) { (stream, pagelet) =>
stream.andThen(renderedPagelets(pagelet.id))
}
}
}
}
================================================
FILE: sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala
================================================
package com.ybrikman.ping
import play.api.test.WithBrowser
import scala.collection.JavaConverters._
/**
* An end-to-end test of the de-duping cache. The Scala and Java sample apps can extend this trait to run all the tests
* in it.
*/
trait BaseDedupeSpec extends PingSpecification {
"The Deduping controller" should {
"dedupe remote calls" in new WithBrowser(app = createTestComponents().app) {
browser.goTo(s"http://localhost:$port/dedupe")
val values = browser.$(".id").getTexts.asScala
// First 3 values should be the same since they were de-duped, fourth should be different
values must have size 4
values(0) mustEqual values(1)
values(1) mustEqual values(2)
values(1) mustNotEqual values(3)
}
}
}
================================================
FILE: sample-app-common/src/test/scala/com/ybrikman/ping/PingSpecification.scala
================================================
package com.ybrikman.ping
import akka.actor.ActorSystem
import org.specs2.execute.{AsResult, Result}
import play.api.Application
import play.api.libs.ws.WSClient
import play.api.routing.Router
import play.api.test.{PlaySpecification, DefaultAwaitTimeout, FutureAwaits, WithServer}
trait PingSpecification extends PlaySpecification with PingTestComponentsProvider
trait PingTestComponentsProvider {
def createTestComponents(customRoutes: Option[RouterComponents => Router] = None): PingTestComponents
}
/**
* Common class that abstracts away whether the underlying app uses Java or Scala and how it's initialized.
*
* @param app
* @param wsClient
*/
case class PingTestComponents(app: Application, wsClient: WSClient)
/**
* A bit of an ugly hack. Some test cases need custom routing. One in particular has a custom action that depends on
* access to the ActorSystem. I can't figure out an easy way to solve this that works with both run-time dependency
* injection (for Java apps) and compile-time dependency injection (for Scala apps), so this is an ugly workaround.
*
* @param actorSystem
*/
case class RouterComponents(actorSystem: ActorSystem)
/**
* An extension of specs2 "Around" that can be used to fire up a test server in one line.
*
* @param components
*/
abstract class WithPingTestServer(val components: PingTestComponents) extends WithServer(app = components.app)
/**
* Same as WithPingTestServer, except this one hits the /warmup URL a bunch of times before running the test. This is
* useful to ensure the app is fully up and running so that tests sensitive to timing are not thrown off by bootup
* and initialization routines.
*
* @param components
*/
abstract class WithWarmedUpPingTestServer(components: PingTestComponents) extends WithPingTestServer(components) with FutureAwaits with DefaultAwaitTimeout {
override def around[T](t: => T)(implicit evidence$3: AsResult[T]): Result = {
super.around {
warmup()
t
}
}
// Make sure the server is warmed up so our timing is not thrown off by bootup and initialization routines
private def warmup(): Unit = {
(0 until 15).foreach { _ =>
await(components.wsClient.url(s"http://localhost:$port/warmup").get())
}
}
}
================================================
FILE: sample-app-java/app/controllers/Deduping.java
================================================
package controllers;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.ybrikman.ping.javaapi.promise.PromiseHelper;
import data.ServiceClient;
import data.UrlAndId;
import play.libs.F;
import play.libs.Json;
import play.libs.ws.WSResponse;
import play.mvc.Controller;
import play.mvc.Result;
import javax.inject.Inject;
/**
* This controller shows an example of how remote service calls can be transparently de-duped using the DedupingCache
* to ensure that we only make one remote call for each unique URL.
*/
public class Deduping extends Controller {
private final ServiceClient serviceClient;
@Inject
public Deduping(ServiceClient serviceClient) {
this.serviceClient = serviceClient;
}
public F.Promise<Result> index() {
// Call an endpoint on this same Play app that returns the request id, which should be unique for every incoming
// request
String baseUrl = "http://" + request().host();
String url1 = baseUrl + "/mock/requestId";
String url2 = baseUrl + "/mock/requestId?foo=bar";
// Thanks to the DedupingCache in the ServiceClient, all 3 calls to url1 will result in only a single remote call
// and the call to url2 will result in a separate call
F.Promise<WSResponse> promise1 = serviceClient.remoteCall(url1);
F.Promise<WSResponse> promise2 = serviceClient.remoteCall(url1);
F.Promise<WSResponse> promise3 = serviceClient.remoteCall(url1);
F.Promise<WSResponse> promise4 = serviceClient.remoteCall(url2);
return PromiseHelper.sequence(promise1, promise2, promise3, promise4).map((result1, result2, result3, result4) -> {
// We should expect to see the same request id for the first 3 requests (since deduping should ensure only one
// request is actually made) and a different id for the fourth one
UrlAndId urlAndId1 = new UrlAndId(url1, result1.getBody());
UrlAndId urlAndId2 = new UrlAndId(url1, result2.getBody());
UrlAndId urlAndId3 = new UrlAndId(url1, result3.getBody());
UrlAndId urlAndId4 = new UrlAndId(url2, result4.getBody());
return ok(views.html.dedupe.apply(urlAndId1, urlAndId2, urlAndId3, urlAndId4));
});
}
}
================================================
FILE: sample-app-java/app/controllers/Mock.java
================================================
package controllers;
import play.mvc.Controller;
import play.mvc.Result;
/**
* This controller is used as a mock remote endpoint in other tests. This allows you to make actual remote calls, but
* still completely control the response you get back.
*/
public class Mock extends Controller {
/**
* An endpoint that returns the request id of the incoming request
*
* @return
*/
public Result requestId() {
return ok(ctx().id().toString());
}
}
================================================
FILE: sample-app-java/app/controllers/MoreBigPipeExamples.java
================================================
package controllers;
import com.fasterxml.jackson.databind.JsonNode;
import com.ybrikman.ping.javaapi.bigpipe.BigPipe;
import com.ybrikman.ping.javaapi.bigpipe.HtmlPagelet;
import com.ybrikman.ping.javaapi.bigpipe.HtmlStreamHelper;
import com.ybrikman.ping.javaapi.bigpipe.JsonPagelet;
import com.ybrikman.ping.javaapi.bigpipe.Pagelet;
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions;
import data.Response;
import helper.FakeServiceClient;
import data.FakeServiceClient$;
import play.libs.F;
import play.mvc.Controller;
import play.mvc.Result;
import play.twirl.api.Html;
import javax.inject.Inject;
/**
* A few more BigPipe examples
*/
public class MoreBigPipeExamples extends Controller {
private final FakeServiceClient serviceClient;
@Inject
public MoreBigPipeExamples(FakeServiceClient serviceClient) {
this.serviceClient = serviceClient;
}
/**
* Renders the exact same page as WithBigPipe#index, but this time with server-side rendering. This will render all
* pagelets server-side and send them down in-order. The page load time will be longer than with out-of-order
* client-side rendering (albeit still faster than not using BigPipe at all), but the advantage is that server-side
* rendering does not depend on JavaScript, which is important for certain use cases (e.g. older browsers, search
* engine crawlers, SEO).
*
* @return
*/
public Result serverSideRendering() {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
F.Promise<Response> profilePromise = serviceClient.fakeRemoteCallMedium("profile");
F.Promise<Response> graphPromise = serviceClient.fakeRemoteCallMedium("graph");
F.Promise<Response> feedPromise = serviceClient.fakeRemoteCallSlow("feed");
F.Promise<Response> inboxPromise = serviceClient.fakeRemoteCallSlow("inbox");
F.Promise<Response> adsPromise = serviceClient.fakeRemoteCallFast("ads");
F.Promise<Response> searchPromise = serviceClient.fakeRemoteCallFast("search");
// Convert each Promise into a Pagelet which will be rendered as HTML as soon as the data is available.
Pagelet profile = new HtmlPagelet("profile", profilePromise.map(views.html.helpers.module::apply));
Pagelet graph = new HtmlPagelet("graph", graphPromise.map(views.html.helpers.module::apply));
Pagelet feed = new HtmlPagelet("feed", feedPromise.map(views.html.helpers.module::apply));
Pagelet inbox = new HtmlPagelet("inbox", inboxPromise.map(views.html.helpers.module::apply));
Pagelet ads = new HtmlPagelet("ads", adsPromise.map(views.html.helpers.module::apply));
Pagelet search = new HtmlPagelet("search", searchPromise.map(views.html.helpers.module::apply));
// Use BigPipe to compose the pagelets and render them immediately using a streaming template. Note that we're using
// ServerSide rendering in this case.
BigPipe bigPipe = new BigPipe(PageletRenderOptions.ServerSide, profile, graph, feed, inbox, ads, search);
return ok(HtmlStreamHelper.toChunks(views.stream.withBigPipe.apply(bigPipe, profile, graph, feed, inbox, ads, search)));
}
/**
* Instead of rendering each pagelet server-side with Play's templating, you can send back JSON and render each
* pagelet with a client-side templating library such as mustache.js
*
* @return
*/
public Result clientSideTemplating() {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
F.Promise<JsonNode> profilePromise = serviceClient.fakeRemoteCallJsonMedium("profile");
F.Promise<JsonNode> graphPromise = serviceClient.fakeRemoteCallJsonMedium("graph");
F.Promise<JsonNode> feedPromise = serviceClient.fakeRemoteCallJsonSlow("feed");
F.Promise<JsonNode> inboxPromise = serviceClient.fakeRemoteCallJsonSlow("inbox");
F.Promise<JsonNode> adsPromise = serviceClient.fakeRemoteCallJsonFast("ads");
F.Promise<JsonNode> searchPromise = serviceClient.fakeRemoteCallJsonFast("search");
Pagelet profile = new JsonPagelet("profile", profilePromise);
Pagelet graph = new JsonPagelet("graph", graphPromise);
Pagelet feed = new JsonPagelet("feed", feedPromise);
Pagelet inbox = new JsonPagelet("inbox", inboxPromise);
Pagelet ads = new JsonPagelet("ads", adsPromise);
Pagelet search = new JsonPagelet("search", searchPromise);
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
BigPipe bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search);
return ok(HtmlStreamHelper.toChunks(views.stream.clientSideTemplating.apply(bigPipe, profile, graph, feed, inbox, ads, search)));
}
/**
* Shows an example of how to handle an error that occurs part way through streaming a response to the browser. Since
* you've already sent back the headers with a 200 OK, it's too late to send back a 500 error page, so instead, you
* have to inject JavaScript into the stream that will show an appropriate error page.
*
* @return
*/
public Result errorHandling() {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
F.Promise<Response> profilePromise = serviceClient.fakeRemoteCallMedium("profile");
F.Promise<Response> graphPromise = serviceClient.fakeRemoteCallMedium("graph");
F.Promise<Response> feedPromise = serviceClient.fakeRemoteCallErrorSlow("feed");
F.Promise<Response> inboxPromise = serviceClient.fakeRemoteCallSlow("inbox");
F.Promise<Response> adsPromise = serviceClient.fakeRemoteCallFast("ads");
F.Promise<Response> searchPromise = serviceClient.fakeRemoteCallFast("search");
// Convert each Promise into a Pagelet which will be rendered as HTML as soon as the data is available. Note that
// the render method used here will also handle the case where the Future completes with an error by rendering an
// error message.
Pagelet profile = new HtmlPagelet("profile", render(profilePromise));
Pagelet graph = new HtmlPagelet("graph", render(graphPromise));
Pagelet feed = new HtmlPagelet("feed", render(feedPromise));
Pagelet inbox = new HtmlPagelet("inbox", render(inboxPromise));
Pagelet ads = new HtmlPagelet("ads", render(adsPromise));
Pagelet search = new HtmlPagelet("search", render(searchPromise));
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
BigPipe bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search);
return ok(HtmlStreamHelper.toChunks(views.stream.withBigPipe.apply(bigPipe, profile, graph, feed, inbox, ads, search)));
}
/**
* Shows an example of how BigPipe escapes the contents of your pagelets so
* they cannot break out of their containing HTML elements (which are
* intentionally invisible).
*
* @return
*/
public Result escaping() {
F.Promise<JsonNode> shouldBeEscapedPromise = serviceClient.fakeRemoteCallJsonFast(FakeServiceClient$.MODULE$.RESPONSE_TO_TEST_ESCAPING());
Pagelet shouldBeEscaped = new JsonPagelet("shouldBeEscaped", shouldBeEscapedPromise);
BigPipe bigPipe = new BigPipe(PageletRenderOptions.ClientSide, shouldBeEscaped);
return ok(HtmlStreamHelper.toChunks(views.stream.escaping.apply(bigPipe, shouldBeEscaped)));
}
/**
* When the given Future redeems, render it with the module template. If the Future fails, render it with the
* error template.
*
* @param dataPromise
* @return
*/
private F.Promise<Html> render(F.Promise<Response> dataPromise) {
return dataPromise.map(views.html.helpers.module::apply).recover(views.html.helpers.error::apply);
}
}
================================================
FILE: sample-app-java/app/controllers/WithBigPipe.java
================================================
package controllers;
import com.ybrikman.ping.javaapi.bigpipe.BigPipe;
import com.ybrikman.ping.javaapi.bigpipe.HtmlPagelet;
import com.ybrikman.ping.javaapi.bigpipe.HtmlStreamHelper;
import com.ybrikman.ping.javaapi.bigpipe.Pagelet;
import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions;
import data.Response;
import helper.FakeServiceClient;
import play.libs.F;
import play.mvc.Controller;
import play.mvc.Result;
import javax.inject.Inject;
public class WithBigPipe extends Controller {
private final FakeServiceClient serviceClient;
@Inject
public WithBigPipe(FakeServiceClient serviceClient) {
this.serviceClient = serviceClient;
}
public Result index() {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
F.Promise<Response> profilePromise = serviceClient.fakeRemoteCallMedium("profile");
F.Promise<Response> graphPromise = serviceClient.fakeRemoteCallMedium("graph");
F.Promise<Response> feedPromise = serviceClient.fakeRemoteCallSlow("feed");
F.Promise<Response> inboxPromise = serviceClient.fakeRemoteCallSlow("inbox");
F.Promise<Response> adsPromise = serviceClient.fakeRemoteCallFast("ads");
F.Promise<Response> searchPromise = serviceClient.fakeRemoteCallFast("search");
// Convert each Promise into a Pagelet which will be rendered as HTML as soon as the data is available.
Pagelet profile = new HtmlPagelet("profile", profilePromise.map(views.html.helpers.module::apply));
Pagelet graph = new HtmlPagelet("graph", graphPromise.map(views.html.helpers.module::apply));
Pagelet feed = new HtmlPagelet("feed", feedPromise.map(views.html.helpers.module::apply));
Pagelet inbox = new HtmlPagelet("inbox", inboxPromise.map(views.html.helpers.module::apply));
Pagelet ads = new HtmlPagelet("ads", adsPromise.map(views.html.helpers.module::apply));
Pagelet search = new HtmlPagelet("search", searchPromise.map(views.html.helpers.module::apply));
// Use BigPipe to compose the pagelets and render them immediately using a streaming template
BigPipe bigPipe = new BigPipe(PageletRenderOptions.ClientSide, profile, graph, feed, inbox, ads, search);
return ok(HtmlStreamHelper.toChunks(views.stream.withBigPipe.apply(bigPipe, profile, graph, feed, inbox, ads, search)));
}
}
================================================
FILE: sample-app-java/app/controllers/WithoutBigPipe.java
================================================
package controllers;
import com.ybrikman.ping.javaapi.promise.PromiseHelper;
import data.Response;
import helper.FakeServiceClient;
import play.libs.F;
import play.mvc.Controller;
import play.mvc.Result;
import javax.inject.Inject;
public class WithoutBigPipe extends Controller {
private final FakeServiceClient serviceClient;
@Inject
public WithoutBigPipe(FakeServiceClient serviceClient) {
this.serviceClient = serviceClient;
}
public F.Promise<Result> index() {
// Make several fake service calls in parallel to represent fetching data from remote backends. Some of the calls
// will be fast, some medium, and some slow.
F.Promise<Response> profilePromise = serviceClient.fakeRemoteCallMedium("profile");
F.Promise<Response> graphPromise = serviceClient.fakeRemoteCallMedium("graph");
F.Promise<Response> feedPromise = serviceClient.fakeRemoteCallSlow("feed");
F.Promise<Response> inboxPromise = serviceClient.fakeRemoteCallSlow("inbox");
F.Promise<Response> adsPromise = serviceClient.fakeRemoteCallFast("ads");
F.Promise<Response> searchPromise = serviceClient.fakeRemoteCallFast("search");
return PromiseHelper
.sequence(profilePromise, graphPromise, feedPromise, inboxPromise, adsPromise, searchPromise)
.map((profile, graph, feed, inbox, ads, search) ->
ok(views.html.withoutBigPipe.apply(profile, graph, feed, inbox, ads, search))
);
}
}
================================================
FILE: sample-app-java/app/data/ServiceClient.java
================================================
package data;
import com.ybrikman.ping.javaapi.dedupe.DedupingCache;
import play.libs.F;
import play.libs.ws.WSClient;
import play.libs.ws.WSResponse;
import javax.inject.Inject;
public class ServiceClient {
private final WSClient ws;
private final DedupingCache<String, F.Promise<WSResponse>> cache;
@Inject
public ServiceClient(WSClient ws, DedupingCache<String, F.Promise<WSResponse>> cache) {
this.ws = ws;
this.cache = cache;
}
public F.Promise<WSResponse> remoteCall(String url) {
return cache.get(url, () -> ws.url(url).get());
}
}
================================================
FILE: sample-app-java/app/helper/FakeServiceClient.java
================================================
package helper;
import akka.actor.ActorSystem;
import com.fasterxml.jackson.databind.JsonNode;
import data.FutureUtil;
import data.Response;
import play.api.libs.json.JsValue;
import play.libs.F;
import play.libs.Json;
import javax.inject.Inject;
public class FakeServiceClient {
private final data.FakeServiceClient delegate;
@Inject
public FakeServiceClient(ActorSystem actorSystem) {
delegate = new data.FakeServiceClient(new FutureUtil(actorSystem));
}
public F.Promise<Response> fakeRemoteCallFast(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallFast(id));
}
public F.Promise<Response> fakeRemoteCallMedium(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallMedium(id));
}
public F.Promise<Response> fakeRemoteCallSlow(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallSlow(id));
}
public F.Promise<Response> fakeRemoteCall(String id, long delayInMillis) {
return F.Promise.wrap(delegate.fakeRemoteCall(id, delayInMillis));
}
public F.Promise<JsonNode> fakeRemoteCallJsonFast(String id) {
return toJsonNode(F.Promise.wrap(delegate.fakeRemoteCallJsonFast(id)));
}
public F.Promise<JsonNode> fakeRemoteCallJsonMedium(String id) {
return toJsonNode(F.Promise.wrap(delegate.fakeRemoteCallJsonMedium(id)));
}
public F.Promise<JsonNode> fakeRemoteCallJsonSlow(String id) {
return toJsonNode(F.Promise.wrap(delegate.fakeRemoteCallJsonSlow(id)));
}
public F.Promise<JsonNode> fakeRemoteCallJson(String id, long delayInMillis) {
return toJsonNode(F.Promise.wrap(delegate.fakeRemoteCallJson(id, delayInMillis)));
}
private F.Promise<JsonNode> toJsonNode(F.Promise<JsValue> jsValuePromise) {
return jsValuePromise.map(jsValue -> Json.parse(jsValue.toString()));
}
public F.Promise<Response> fakeRemoteCallErrorFast(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallErrorFast(id));
}
public F.Promise<Response> fakeRemoteCallErrorMedium(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallErrorMedium(id));
}
public F.Promise<Response> fakeRemoteCallErrorSlow(String id) {
return F.Promise.wrap(delegate.fakeRemoteCallErrorSlow(id));
}
public F.Promise<Response> fakeRemoteCallError(String id, long delayInMillis) {
return F.Promise.wrap(delegate.fakeRemoteCallError(id, delayInMillis));
}
}
================================================
FILE: sample-app-java/app/loader/Filters.java
================================================
package loader;
import com.ybrikman.ping.javaapi.dedupe.CacheFilter;
import com.ybrikman.ping.javaapi.dedupe.DedupingCache;
import play.api.mvc.EssentialFilter;
import play.http.HttpFilters;
import play.libs.F;
import play.libs.ws.WSResponse;
import javax.inject.Inject;
/**
* Custom Filters for this app. Play knows to load this class because we put it into conf/application.conf
*/
public class Filters implements HttpFilters {
private final CacheFilter<String, F.Promise<WSResponse>> cacheFilter;
@Inject
public Filters(DedupingCache<String, F.Promise<WSRespons
gitextract_o11b8m2f/ ├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── big-pipe/ │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── ybrikman/ │ │ │ └── ping/ │ │ │ └── javaapi/ │ │ │ ├── bigpipe/ │ │ │ │ ├── BigPipe.java │ │ │ │ ├── HtmlPagelet.java │ │ │ │ ├── HtmlStreamHelper.java │ │ │ │ ├── JsonPagelet.java │ │ │ │ ├── Pagelet.java │ │ │ │ ├── PageletContentType.java │ │ │ │ ├── PageletRenderOptions.java │ │ │ │ └── TextPagelet.java │ │ │ ├── dedupe/ │ │ │ │ ├── CacheFilter.java │ │ │ │ └── DedupingCache.java │ │ │ └── promise/ │ │ │ ├── Function2.java │ │ │ ├── Function3.java │ │ │ ├── Function4.java │ │ │ ├── Function5.java │ │ │ ├── Function6.java │ │ │ ├── Promise2.java │ │ │ ├── Promise3.java │ │ │ ├── Promise4.java │ │ │ ├── Promise5.java │ │ │ ├── Promise6.java │ │ │ └── PromiseHelper.java │ │ ├── resources/ │ │ │ └── public/ │ │ │ └── com/ │ │ │ └── ybrikman/ │ │ │ └── ping/ │ │ │ └── big-pipe.js │ │ ├── scala/ │ │ │ └── com/ │ │ │ └── ybrikman/ │ │ │ └── ping/ │ │ │ └── scalaapi/ │ │ │ ├── bigpipe/ │ │ │ │ ├── BigPipe.scala │ │ │ │ ├── Embed.scala │ │ │ │ ├── HtmlStream.scala │ │ │ │ ├── JavaAdapter.scala │ │ │ │ └── Pagelet.scala │ │ │ ├── compose/ │ │ │ │ └── Compose.scala │ │ │ └── dedupe/ │ │ │ ├── BeforeAndAfterFilter.scala │ │ │ ├── Cache.scala │ │ │ ├── CacheFilter.scala │ │ │ ├── CacheNotInitializedException.scala │ │ │ └── DedupingCache.scala │ │ └── twirl/ │ │ └── com/ │ │ └── ybrikman/ │ │ └── bigpipe/ │ │ ├── css.scala.html │ │ ├── js.scala.html │ │ ├── pageletClientSide.scala.html │ │ └── pageletServerSide.scala.html │ └── test/ │ └── scala/ │ └── com/ │ └── ybrikman/ │ └── ping/ │ ├── javaapi/ │ │ └── dedupe/ │ │ ├── TestCacheFilter.scala │ │ └── TestDedupingCache.scala │ └── scalaapi/ │ ├── bigpipe/ │ │ ├── TestBigPipeJavaScript.scala │ │ └── TestEmbed.scala │ └── dedupe/ │ ├── TestBeforeAndAfterFilter.scala │ ├── TestCache.scala │ ├── TestCacheFilter.scala │ └── TestDedupingCache.scala ├── build.sbt ├── circle.yml ├── docker-compose.yml ├── project/ │ ├── build.properties │ └── plugins.sbt ├── sample-app-common/ │ └── src/ │ ├── main/ │ │ ├── resources/ │ │ │ └── public/ │ │ │ ├── javascripts/ │ │ │ │ ├── big-pipe-with-mustache.js │ │ │ │ ├── mustache.js │ │ │ │ └── timing.js │ │ │ └── stylesheets/ │ │ │ └── main.css │ │ ├── scala/ │ │ │ └── data/ │ │ │ ├── FakeServiceClient.scala │ │ │ ├── FutureUtil.scala │ │ │ ├── Response.scala │ │ │ └── UrlAndId.scala │ │ └── twirl/ │ │ └── views/ │ │ ├── clientSideTemplating.scala.stream │ │ ├── dedupe.scala.html │ │ ├── escaping.scala.stream │ │ ├── helpers/ │ │ │ ├── error.scala.html │ │ │ ├── module.scala.html │ │ │ └── timing.scala.html │ │ ├── withBigPipe.scala.stream │ │ └── withoutBigPipe.scala.html │ └── test/ │ └── scala/ │ └── com/ │ └── ybrikman/ │ └── ping/ │ ├── BaseBigPipeSpec.scala │ ├── BaseBigPipeTimingSpec.scala │ ├── BaseDedupeSpec.scala │ └── PingSpecification.scala ├── sample-app-java/ │ ├── app/ │ │ ├── controllers/ │ │ │ ├── Deduping.java │ │ │ ├── Mock.java │ │ │ ├── MoreBigPipeExamples.java │ │ │ ├── WithBigPipe.java │ │ │ └── WithoutBigPipe.java │ │ ├── data/ │ │ │ └── ServiceClient.java │ │ ├── helper/ │ │ │ └── FakeServiceClient.java │ │ └── loader/ │ │ └── Filters.java │ ├── conf/ │ │ ├── application.conf │ │ └── routes │ └── test/ │ └── com/ │ └── ybrikman/ │ └── ping/ │ ├── PingJavaTestComponents.scala │ └── Tests.scala ├── sample-app-scala/ │ ├── app/ │ │ ├── controllers/ │ │ │ ├── Deduping.scala │ │ │ ├── Mock.scala │ │ │ ├── MoreBigPipeExamples.scala │ │ │ ├── WithBigPipe.scala │ │ │ └── WithoutBigPipe.scala │ │ ├── data/ │ │ │ └── ServiceClient.scala │ │ └── loader/ │ │ └── PingApplicationLoader.scala │ ├── conf/ │ │ ├── application.conf │ │ └── routes │ └── test/ │ └── com/ │ └── ybrikman/ │ └── ping/ │ ├── PingScalaTestComponents.scala │ └── Tests.scala └── version.sbt
SYMBOL INDEX (143 symbols across 30 files)
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/BigPipe.java
class BigPipe (line 18) | public class BigPipe extends com.ybrikman.ping.scalaapi.bigpipe.BigPipe {
method BigPipe (line 20) | public BigPipe(PageletRenderOptions renderOptions, List<Pagelet> pagel...
method BigPipe (line 24) | public BigPipe(PageletRenderOptions renderOptions, List<Pagelet> pagel...
method BigPipe (line 28) | public BigPipe(PageletRenderOptions renderOptions, Pagelet ... pagelet...
method toScalaPagelets (line 32) | private static scala.collection.immutable.List<com.ybrikman.ping.scala...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlPagelet.java
class HtmlPagelet (line 10) | public class HtmlPagelet implements Pagelet {
method HtmlPagelet (line 15) | public HtmlPagelet(String id, F.Promise<Html> content) {
method id (line 20) | @Override
method wrapped (line 25) | @Override
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlStreamHelper.java
class HtmlStreamHelper (line 20) | public class HtmlStreamHelper {
method empty (line 22) | public static HtmlStream empty() {
method fromString (line 26) | public static HtmlStream fromString(String str) {
method fromHtml (line 30) | public static HtmlStream fromHtml(Html html) {
method fromHtmlEnumerator (line 34) | public static HtmlStream fromHtmlEnumerator(Enumerator<Html> enumerato...
method fromHtmlPromise (line 38) | public static HtmlStream fromHtmlPromise(Promise<Html> html) {
method fromHtmlPromise (line 42) | public static HtmlStream fromHtmlPromise(Promise<Html> html, Execution...
method fromResult (line 46) | public static HtmlStream fromResult(Result result) {
method fromResult (line 50) | public static HtmlStream fromResult(Result result, ExecutionContext ec...
method fromResultPromise (line 54) | public static HtmlStream fromResultPromise(Promise<Result> result) {
method fromResultPromise (line 58) | public static HtmlStream fromResultPromise(Promise<Result> result, Exe...
method flatten (line 62) | public static HtmlStream flatten(Promise<HtmlStream> stream) {
method flatten (line 66) | public static HtmlStream flatten(Promise<HtmlStream> stream, Execution...
method interleave (line 70) | public static HtmlStream interleave(HtmlStream ... streams) {
method interleave (line 74) | public static HtmlStream interleave(List<HtmlStream> streams) {
method toChunks (line 78) | public static Results.Chunks<Html> toChunks(HtmlStream stream) {
method toChunks (line 82) | public static Results.Chunks<Html> toChunks(HtmlStream stream, Codec c...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/JsonPagelet.java
class JsonPagelet (line 13) | public class JsonPagelet implements Pagelet {
method JsonPagelet (line 17) | public JsonPagelet(String id, F.Promise<JsonNode> content) {
method id (line 22) | @Override
method wrapped (line 27) | @Override
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/Pagelet.java
type Pagelet (line 11) | public interface Pagelet extends com.ybrikman.ping.scalaapi.bigpipe.Page...
method renderPlaceholder (line 13) | default public HtmlStream renderPlaceholder() {
method renderServerSide (line 17) | default public HtmlStream renderServerSide() {
method renderClientSide (line 21) | default public HtmlStream renderClientSide() {
method renderPlaceholder (line 25) | @Override
method renderServerSide (line 30) | @Override
method renderClientSide (line 35) | @Override
method wrapped (line 40) | com.ybrikman.ping.scalaapi.bigpipe.Pagelet wrapped(ExecutionContext ec);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletContentType.java
type PageletContentType (line 6) | public enum PageletContentType {
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletRenderOptions.java
type PageletRenderOptions (line 8) | public enum PageletRenderOptions {
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/TextPagelet.java
class TextPagelet (line 9) | public class TextPagelet implements Pagelet {
method TextPagelet (line 13) | public TextPagelet(String id, F.Promise<String> content) {
method id (line 18) | @Override
method wrapped (line 23) | @Override
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/CacheFilter.java
class CacheFilter (line 18) | public class CacheFilter<K, V> extends BeforeAndAfterFilter {
method CacheFilter (line 19) | public CacheFilter(DedupingCache<K, V> cache) {
method CacheFilter (line 23) | public CacheFilter(DedupingCache<K, V> cache, ExecutionContext executi...
method contextFromRequestHeader (line 30) | private static Http.Context contextFromRequestHeader(play.api.mvc.Requ...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/DedupingCache.java
class DedupingCache (line 35) | @Singleton
method DedupingCache (line 39) | public DedupingCache() {
method get (line 51) | public V get(K key, Supplier<V> valueIfMissing) {
method get (line 65) | public V get(K key, Supplier<V> valueIfMissing, Http.Context context) {
method initCacheForRequest (line 74) | public void initCacheForRequest(Http.Context context) {
method cleanupCacheForRequest (line 84) | public void cleanupCacheForRequest(Http.Context context) {
method getCacheForPlayRequest (line 88) | private Cache<K, V> getCacheForPlayRequest(Http.Context context) {
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function2.java
type Function2 (line 3) | @FunctionalInterface
method apply (line 5) | public R apply(A a, B b);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function3.java
type Function3 (line 3) | @FunctionalInterface
method apply (line 5) | public R apply(A a, B b, C c);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function4.java
type Function4 (line 3) | @FunctionalInterface
method apply (line 5) | public R apply(A a, B b, C c, D d);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function5.java
type Function5 (line 3) | @FunctionalInterface
method apply (line 5) | public R apply(A a, B b, C c, D d, E e);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function6.java
type Function6 (line 3) | @FunctionalInterface
method apply (line 5) | public R apply(A a, B b, C c, D d, E e, F f);
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise2.java
class Promise2 (line 5) | public class Promise2<A, B> {
method Promise2 (line 10) | public Promise2(F.Promise<A> a, F.Promise<B> b) {
method map (line 15) | public <R> F.Promise<R> map(Function2<A, B, R> function) {
method flatMap (line 19) | public <R> F.Promise<R> flatMap(Function2<A, B, F.Promise<R>> function) {
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise3.java
class Promise3 (line 5) | public class Promise3<A, B, C> {
method Promise3 (line 11) | public Promise3(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c) {
method map (line 17) | public <R> F.Promise<R> map(Function3<A, B, C, R> function) {
method flatMap (line 21) | public <R> F.Promise<R> flatMap(Function3<A, B, C, F.Promise<R>> funct...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise4.java
class Promise4 (line 5) | public class Promise4<A, B, C, D> {
method Promise4 (line 12) | public Promise4(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c, F.Prom...
method map (line 19) | public <R> F.Promise<R> map(Function4<A, B, C, D, R> function) {
method flatMap (line 23) | public <R> F.Promise<R> flatMap(Function4<A, B, C, D, F.Promise<R>> fu...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise5.java
class Promise5 (line 5) | public class Promise5<A, B, C, D, E> {
method Promise5 (line 13) | public Promise5(F.Promise<A> a, F.Promise<B> b, F.Promise<C> c, F.Prom...
method map (line 21) | public <R> F.Promise<R> map(Function5<A, B, C, D, E, R> function) {
method flatMap (line 25) | public <R> F.Promise<R> flatMap(Function5<A, B, C, D, E, F.Promise<R>>...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise6.java
class Promise6 (line 5) | public class Promise6<A, B, C, D, E, F> {
method Promise6 (line 14) | public Promise6(play.libs.F.Promise<A> a, play.libs.F.Promise<B> b, pl...
method map (line 23) | public <R> play.libs.F.Promise<R> map(Function6<A, B, C, D, E, F, R> f...
method flatMap (line 27) | public <R> play.libs.F.Promise<R> flatMap(Function6<A, B, C, D, E, F, ...
FILE: big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/PromiseHelper.java
class PromiseHelper (line 5) | public class PromiseHelper {
method sequence (line 7) | public static <A, B> Promise2<A, B> sequence(Promise<A> a, Promise<B> ...
method sequence (line 11) | public static <A, B, C> Promise3<A, B, C> sequence(Promise<A> a, Promi...
method sequence (line 15) | public static <A, B, C, D> Promise4<A, B, C, D> sequence(Promise<A> a,...
method sequence (line 19) | public static <A, B, C, D, E> Promise5<A, B, C, D, E> sequence(Promise...
method sequence (line 23) | public static <A, B, C, D, E, F> Promise6<A, B, C, D, E, F> sequence(P...
FILE: sample-app-common/src/main/resources/public/javascripts/mustache.js
function isFunction (line 24) | function isFunction (object) {
function escapeRegExp (line 28) | function escapeRegExp (string) {
function hasProperty (line 36) | function hasProperty (obj, propName) {
function testRegExp (line 43) | function testRegExp (re, string) {
function isWhitespace (line 48) | function isWhitespace (string) {
function escapeHtml (line 61) | function escapeHtml (string) {
function parseTemplate (line 95) | function parseTemplate (template, tags) {
function squashTokens (line 225) | function squashTokens (tokens) {
function nestTokens (line 252) | function nestTokens (tokens) {
function Scanner (line 285) | function Scanner (string) {
function Context (line 345) | function Context (view, parentContext) {
function Writer (line 420) | function Writer () {
function subRender (line 499) | function subRender (template) {
FILE: sample-app-java/app/controllers/Deduping.java
class Deduping (line 19) | public class Deduping extends Controller {
method Deduping (line 22) | @Inject
method index (line 27) | public F.Promise<Result> index() {
FILE: sample-app-java/app/controllers/Mock.java
class Mock (line 10) | public class Mock extends Controller {
method requestId (line 17) | public Result requestId() {
FILE: sample-app-java/app/controllers/MoreBigPipeExamples.java
class MoreBigPipeExamples (line 23) | public class MoreBigPipeExamples extends Controller {
method MoreBigPipeExamples (line 26) | @Inject
method serverSideRendering (line 40) | public Result serverSideRendering() {
method clientSideTemplating (line 70) | public Result clientSideTemplating() {
method errorHandling (line 99) | public Result errorHandling() {
method escaping (line 131) | public Result escaping() {
method render (line 148) | private F.Promise<Html> render(F.Promise<Response> dataPromise) {
FILE: sample-app-java/app/controllers/WithBigPipe.java
class WithBigPipe (line 16) | public class WithBigPipe extends Controller {
method WithBigPipe (line 19) | @Inject
method index (line 24) | public Result index() {
FILE: sample-app-java/app/controllers/WithoutBigPipe.java
class WithoutBigPipe (line 12) | public class WithoutBigPipe extends Controller {
method WithoutBigPipe (line 16) | @Inject
method index (line 21) | public F.Promise<Result> index() {
FILE: sample-app-java/app/data/ServiceClient.java
class ServiceClient (line 10) | public class ServiceClient {
method ServiceClient (line 14) | @Inject
method remoteCall (line 20) | public F.Promise<WSResponse> remoteCall(String url) {
FILE: sample-app-java/app/helper/FakeServiceClient.java
class FakeServiceClient (line 13) | public class FakeServiceClient {
method FakeServiceClient (line 16) | @Inject
method fakeRemoteCallFast (line 21) | public F.Promise<Response> fakeRemoteCallFast(String id) {
method fakeRemoteCallMedium (line 25) | public F.Promise<Response> fakeRemoteCallMedium(String id) {
method fakeRemoteCallSlow (line 29) | public F.Promise<Response> fakeRemoteCallSlow(String id) {
method fakeRemoteCall (line 33) | public F.Promise<Response> fakeRemoteCall(String id, long delayInMilli...
method fakeRemoteCallJsonFast (line 37) | public F.Promise<JsonNode> fakeRemoteCallJsonFast(String id) {
method fakeRemoteCallJsonMedium (line 41) | public F.Promise<JsonNode> fakeRemoteCallJsonMedium(String id) {
method fakeRemoteCallJsonSlow (line 45) | public F.Promise<JsonNode> fakeRemoteCallJsonSlow(String id) {
method fakeRemoteCallJson (line 49) | public F.Promise<JsonNode> fakeRemoteCallJson(String id, long delayInM...
method toJsonNode (line 53) | private F.Promise<JsonNode> toJsonNode(F.Promise<JsValue> jsValuePromi...
method fakeRemoteCallErrorFast (line 57) | public F.Promise<Response> fakeRemoteCallErrorFast(String id) {
method fakeRemoteCallErrorMedium (line 61) | public F.Promise<Response> fakeRemoteCallErrorMedium(String id) {
method fakeRemoteCallErrorSlow (line 65) | public F.Promise<Response> fakeRemoteCallErrorSlow(String id) {
method fakeRemoteCallError (line 69) | public F.Promise<Response> fakeRemoteCallError(String id, long delayIn...
FILE: sample-app-java/app/loader/Filters.java
class Filters (line 15) | public class Filters implements HttpFilters {
method Filters (line 19) | @Inject
method filters (line 24) | @Override
Condensed preview — 99 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (221K chars).
[
{
"path": ".dockerignore",
"chars": 277,
"preview": "config\n.project\n.settings\n.classpath\n.cache\n*.iml\n*.iws\n*.ipr\n.idea/\nbuild/\n*/build/\nout/\n*/bin/\nbin\ncodegen/\n*Generated"
},
{
"path": ".gitignore",
"chars": 280,
"preview": "config\n.project\n.settings\n.classpath\n.cache\n*.iml\n*.iws\n*.ipr\n.idea/\nbuild/\n*/build/\nout/\n*/bin/\nbin\ncodegen/\n*Generated"
},
{
"path": "Dockerfile",
"chars": 1168,
"preview": "# Based off an image that has JDK8 on busybox\nFROM frolvlad/alpine-oraclejdk8:cleaned\nMAINTAINER Yevgeniy Brikman <jim@y"
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Yevgeniy Brikman\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 29798,
"preview": "# Ping-Play\n\nThe ping-play project brings [BigPipe](https://www.facebook.com/note.php?note_id=389414033919) streaming to"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/BigPipe.java",
"chars": 1713,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport play.libs.HttpExecution;\nimport scala.collection.JavaConverters;\nimpo"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlPagelet.java",
"chars": 702,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport play.libs.F;\nimport play.twirl.api.Html;\nimport scala.concurrent.Exec"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/HtmlStreamHelper.java",
"chars": 2998,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport com.ybrikman.ping.scalaapi.bigpipe.HtmlStream;\nimport com.ybrikman.pi"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/JsonPagelet.java",
"chars": 1035,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.ybrikman.ping.sca"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/Pagelet.java",
"chars": 1208,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport com.ybrikman.ping.scalaapi.bigpipe.HtmlStream;\nimport play.libs.HttpE"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletContentType.java",
"chars": 201,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\n/**\n * The supported content types that you can have in a pagelet for BigPip"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/PageletRenderOptions.java",
"chars": 454,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\n/**\n * Specify the type of rendering you wish to use with BigPipe: either cl"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/bigpipe/TextPagelet.java",
"chars": 683,
"preview": "package com.ybrikman.ping.javaapi.bigpipe;\n\nimport play.libs.F;\nimport scala.concurrent.ExecutionContext;\n\n/**\n * A Page"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/CacheFilter.java",
"chars": 1385,
"preview": "package com.ybrikman.ping.javaapi.dedupe;\n\nimport com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;\nimport com.ybrikman.pi"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/dedupe/DedupingCache.java",
"chars": 4171,
"preview": "package com.ybrikman.ping.javaapi.dedupe;\n\nimport com.ybrikman.ping.scalaapi.bigpipe.JavaAdapter;\nimport com.ybrikman.pi"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function2.java",
"chars": 133,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\n@FunctionalInterface\npublic interface Function2<A, B, R> {\n public R apply("
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function3.java",
"chars": 141,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\n@FunctionalInterface\npublic interface Function3<A, B, C, R> {\n public R app"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function4.java",
"chars": 149,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\n@FunctionalInterface\npublic interface Function4<A, B, C, D, R> {\n public R "
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function5.java",
"chars": 156,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\n@FunctionalInterface\npublic interface Function5<A, B, C, D, E, R> {\n public"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Function6.java",
"chars": 165,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\n@FunctionalInterface\npublic interface Function6<A, B, C, D, E, F, R> {\n pub"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise2.java",
"chars": 524,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F;\n\npublic class Promise2<A, B> {\n\n private final F.Promis"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise3.java",
"chars": 635,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F;\n\npublic class Promise3<A, B, C> {\n\n private final F.Pro"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise4.java",
"chars": 746,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F;\n\npublic class Promise4<A, B, C, D> {\n\n private final F."
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise5.java",
"chars": 857,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F;\n\npublic class Promise5<A, B, C, D, E> {\n\n private final"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/Promise6.java",
"chars": 1118,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F;\n\npublic class Promise6<A, B, C, D, E, F> {\n\n private fi"
},
{
"path": "big-pipe/src/main/java/com/ybrikman/ping/javaapi/promise/PromiseHelper.java",
"chars": 919,
"preview": "package com.ybrikman.ping.javaapi.promise;\n\nimport play.libs.F.Promise;\n\npublic class PromiseHelper {\n\n public static <"
},
{
"path": "big-pipe/src/main/resources/public/com/ybrikman/ping/big-pipe.js",
"chars": 1971,
"preview": "/**\n * JavaScript helper functions for BigPipe-style streaming. This code has no external dependencies.\n */\n(function() "
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/BigPipe.scala",
"chars": 1700,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions\nimport com.ybr"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/Embed.scala",
"chars": 1312,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport play.api.libs.json.{Json, JsValue}\nimport play.twirl.api.Html\n\nobject"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/HtmlStream.scala",
"chars": 4588,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport play.api.libs.iteratee.{Enumeratee, Enumerator}\nimport play.twirl.api"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/JavaAdapter.scala",
"chars": 1216,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport java.util.function.{Supplier => JavaSupplier, Consumer => JavaConsume"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/bigpipe/Pagelet.scala",
"chars": 3828,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport com.ybrikman.ping.javaapi.bigpipe.PageletContentType\nimport play.api."
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/compose/Compose.scala",
"chars": 4431,
"preview": "package com.ybrikman.ping.scalaapi.compose\n\nimport com.ybrikman.ping.scalaapi.bigpipe.HtmlStream\nimport play.api.http.He"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/BeforeAndAfterFilter.scala",
"chars": 1202,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport play.api.mvc.{Result, RequestHeader, Filter}\n\nimport scala.concurrent."
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/Cache.scala",
"chars": 6219,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport Cache._\nimport play.api.Configuration\nimport java.util.concurrent.Conc"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/CacheFilter.scala",
"chars": 800,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport scala.concurrent.ExecutionContext\n\n/**\n * Any time you use the Dedupin"
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/CacheNotInitializedException.scala",
"chars": 129,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nclass CacheNotInitializedException(message: String) extends RuntimeException("
},
{
"path": "big-pipe/src/main/scala/com/ybrikman/ping/scalaapi/dedupe/DedupingCache.scala",
"chars": 3328,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport play.api.mvc.RequestHeader\n\n/**\n * A cache you can use to de-dupe expe"
},
{
"path": "big-pipe/src/main/twirl/com/ybrikman/bigpipe/css.scala.html",
"chars": 82,
"preview": "@(urls: Seq[String])\n\n@for(url <- urls) {\n <link rel=\"stylesheet\" href=\"@url\"/>\n}"
},
{
"path": "big-pipe/src/main/twirl/com/ybrikman/bigpipe/js.scala.html",
"chars": 97,
"preview": "@(urls: Seq[String])\n\n@for(url <- urls) {\n <script src=\"@url\" type=\"text/javascript\"></script>\n}"
},
{
"path": "big-pipe/src/main/twirl/com/ybrikman/bigpipe/pageletClientSide.scala.html",
"chars": 413,
"preview": "@(content: String, id: String, contentType: com.ybrikman.ping.javaapi.bigpipe.PageletContentType)\n\n@import com.ybrikman."
},
{
"path": "big-pipe/src/main/twirl/com/ybrikman/bigpipe/pageletServerSide.scala.html",
"chars": 66,
"preview": "@(id: String, content: String)\n\n<div id=\"@id\">@Html(content)</div>"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/javaapi/dedupe/TestCacheFilter.scala",
"chars": 2316,
"preview": "package com.ybrikman.ping.javaapi.dedupe\n\nimport java.util.function.Supplier\n\nimport com.ybrikman.ping.scalaapi.dedupe.C"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/javaapi/dedupe/TestDedupingCache.scala",
"chars": 4328,
"preview": "package com.ybrikman.ping.javaapi.dedupe\n\nimport java.util.concurrent.atomic.AtomicInteger\nimport java.util.function.Sup"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/bigpipe/TestBigPipeJavaScript.scala",
"chars": 2189,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport javax.script.{ScriptEngine, ScriptEngineManager}\n\nimport jdk.nashorn."
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/bigpipe/TestEmbed.scala",
"chars": 627,
"preview": "package com.ybrikman.ping.scalaapi.bigpipe\n\nimport org.specs2.mutable.Specification\nimport play.twirl.api.Html\n\nclass Te"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestBeforeAndAfterFilter.scala",
"chars": 5246,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport java.util.concurrent.CopyOnWriteArrayList\n\nimport org.specs2.mutable.S"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestCache.scala",
"chars": 10617,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.specs2.mutable.S"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestCacheFilter.scala",
"chars": 1841,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport org.specs2.mutable.Specification\nimport play.api.libs.concurrent.Execu"
},
{
"path": "big-pipe/src/test/scala/com/ybrikman/ping/scalaapi/dedupe/TestDedupingCache.scala",
"chars": 3617,
"preview": "package com.ybrikman.ping.scalaapi.dedupe\n\nimport java.util.concurrent.atomic.AtomicInteger\n\nimport org.specs2.mutable.S"
},
{
"path": "build.sbt",
"chars": 6094,
"preview": "import ReleaseTransformations._\n\n// The BigPipe library\nlazy val bigPipe = (project in file(\"big-pipe\"))\n .settings(big"
},
{
"path": "circle.yml",
"chars": 827,
"preview": "machine:\n services:\n - docker\n\ndependencies:\n cache_directories:\n - \"~/docker-compose-1.2.0\"\n - \"~/docker"
},
{
"path": "docker-compose.yml",
"chars": 105,
"preview": "web: \n image: brikis98/ping-play\n volumes:\n - .:/src\n ports:\n - \"9000:9000\"\n stdin_open: true\n"
},
{
"path": "project/build.properties",
"chars": 19,
"preview": "sbt.version=0.13.8\n"
},
{
"path": "project/plugins.sbt",
"chars": 397,
"preview": "logLevel := Level.Warn\n\nresolvers += \"Typesafe repository\" at \"http://repo.typesafe.com/typesafe/releases/\"\n\naddSbtPlugi"
},
{
"path": "sample-app-common/src/main/resources/public/javascripts/big-pipe-with-mustache.js",
"chars": 904,
"preview": "(function(window, mustache, BigPipe) {\n \"use strict\";\n\n var document = window.document;\n var console = window.console"
},
{
"path": "sample-app-common/src/main/resources/public/javascripts/mustache.js",
"chars": 18759,
"preview": "/*!\n * mustache.js - Logic-less {{mustache}} templates with JavaScript\n * http://github.com/janl/mustache.js\n */\n\n/*glob"
},
{
"path": "sample-app-common/src/main/resources/public/javascripts/timing.js",
"chars": 832,
"preview": "// Quick, hacky code used to display page load timing using the Navigation Timing API\n(function(window) {\n \"use strict\""
},
{
"path": "sample-app-common/src/main/resources/public/stylesheets/main.css",
"chars": 520,
"preview": "body {\n padding: 20px;\n}\n\n.wrapper td {\n width: 200px;\n height: 160px;\n border: 1px solid #CCC;\n border-radius: 5px"
},
{
"path": "sample-app-common/src/main/scala/data/FakeServiceClient.scala",
"chars": 2380,
"preview": "package data\n\nimport play.api.libs.concurrent.Execution.Implicits._\nimport play.api.libs.json.{Json, JsValue}\n\nimport sc"
},
{
"path": "sample-app-common/src/main/scala/data/FutureUtil.scala",
"chars": 1526,
"preview": "package data\n\nimport java.util.concurrent.TimeUnit\nimport java.util.function.Supplier\n\nimport akka.actor.ActorSystem\nimp"
},
{
"path": "sample-app-common/src/main/scala/data/Response.scala",
"chars": 272,
"preview": "package data\n\nimport play.api.libs.json.Json\n\n/**\n * Simple class used to represent a response from a remote service\n *\n"
},
{
"path": "sample-app-common/src/main/scala/data/UrlAndId.scala",
"chars": 152,
"preview": "package data\n\n/**\n * Silly container class to pass around the URL and ID\n *\n * @param url\n * @param id\n */\ncase class Ur"
},
{
"path": "sample-app-common/src/main/twirl/views/clientSideTemplating.scala.stream",
"chars": 1147,
"preview": "@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)\n\n<ht"
},
{
"path": "sample-app-common/src/main/twirl/views/dedupe.scala.html",
"chars": 1127,
"preview": "@(result1: data.UrlAndId, result2: data.UrlAndId, result3: data.UrlAndId, result4: data.UrlAndId)\n\n<html>\n <head>\n <"
},
{
"path": "sample-app-common/src/main/twirl/views/escaping.scala.stream",
"chars": 710,
"preview": "@(bigPipe: BigPipe, shouldBeEscaped: Pagelet)\n\n<html>\n <head>\n <link rel=\"stylesheet\" href=\"/assets/stylesheets/main"
},
{
"path": "sample-app-common/src/main/twirl/views/helpers/error.scala.html",
"chars": 177,
"preview": "@(error: Throwable)\n\n<div class=\"module error\">\n <h5>There was an</h5>\n <h2 class=\"highlight id\">error</h2>\n <h5>fetc"
},
{
"path": "sample-app-common/src/main/twirl/views/helpers/module.scala.html",
"chars": 178,
"preview": "@(response: data.Response)\n\n<div class=\"module\">\n <h3 class=\"id\">@response.id</h3>\n <h6>took</h6>\n <h2 class=\"highlig"
},
{
"path": "sample-app-common/src/main/twirl/views/helpers/timing.scala.html",
"chars": 385,
"preview": "<!-- This code is included only to display timings -->\n<table id=\"timing\">\n <tr>\n <td><h4>Time to first byte:</h4></"
},
{
"path": "sample-app-common/src/main/twirl/views/withBigPipe.scala.stream",
"chars": 1121,
"preview": "@(bigPipe: BigPipe, profile: Pagelet, graph: Pagelet, feed: Pagelet, inbox: Pagelet, ads: Pagelet, search: Pagelet)\n\n<ht"
},
{
"path": "sample-app-common/src/main/twirl/views/withoutBigPipe.scala.html",
"chars": 839,
"preview": "@(profile: data.Response, graph: data.Response, feed: data.Response, inbox: data.Response, ads: data.Response, search: d"
},
{
"path": "sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeSpec.scala",
"chars": 1910,
"preview": "package com.ybrikman.ping\n\nimport play.api.test.WithBrowser\nimport data.FakeServiceClient\n\n/**\n * End-to-end tests of Bi"
},
{
"path": "sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala",
"chars": 9811,
"preview": "package com.ybrikman.ping\n\nimport com.ybrikman.ping.TimingHelper._\nimport com.ybrikman.ping.CustomRoutes._\nimport com.yb"
},
{
"path": "sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala",
"chars": 763,
"preview": "package com.ybrikman.ping\n\nimport play.api.test.WithBrowser\nimport scala.collection.JavaConverters._\n\n/**\n * An end-to-e"
},
{
"path": "sample-app-common/src/test/scala/com/ybrikman/ping/PingSpecification.scala",
"chars": 2250,
"preview": "package com.ybrikman.ping\n\nimport akka.actor.ActorSystem\nimport org.specs2.execute.{AsResult, Result}\nimport play.api.Ap"
},
{
"path": "sample-app-java/app/controllers/Deduping.java",
"chars": 2180,
"preview": "package controllers;\n\nimport com.fasterxml.jackson.databind.node.ArrayNode;\nimport com.ybrikman.ping.javaapi.promise.Pro"
},
{
"path": "sample-app-java/app/controllers/Mock.java",
"chars": 469,
"preview": "package controllers;\n\nimport play.mvc.Controller;\nimport play.mvc.Result;\n\n/**\n * This controller is used as a mock remo"
},
{
"path": "sample-app-java/app/controllers/MoreBigPipeExamples.java",
"chars": 7949,
"preview": "package controllers;\n\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.ybrikman.ping.javaapi.bigpipe.BigPipe;\n"
},
{
"path": "sample-app-java/app/controllers/WithBigPipe.java",
"chars": 2390,
"preview": "package controllers;\n\nimport com.ybrikman.ping.javaapi.bigpipe.BigPipe;\nimport com.ybrikman.ping.javaapi.bigpipe.HtmlPag"
},
{
"path": "sample-app-java/app/controllers/WithoutBigPipe.java",
"chars": 1441,
"preview": "package controllers;\n\nimport com.ybrikman.ping.javaapi.promise.PromiseHelper;\nimport data.Response;\nimport helper.FakeSe"
},
{
"path": "sample-app-java/app/data/ServiceClient.java",
"chars": 570,
"preview": "package data;\n\nimport com.ybrikman.ping.javaapi.dedupe.DedupingCache;\nimport play.libs.F;\nimport play.libs.ws.WSClient;\n"
},
{
"path": "sample-app-java/app/helper/FakeServiceClient.java",
"chars": 2357,
"preview": "package helper;\n\nimport akka.actor.ActorSystem;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport data.FutureUtil;\n"
},
{
"path": "sample-app-java/app/loader/Filters.java",
"chars": 743,
"preview": "package loader;\n\nimport com.ybrikman.ping.javaapi.dedupe.CacheFilter;\nimport com.ybrikman.ping.javaapi.dedupe.DedupingCa"
},
{
"path": "sample-app-java/conf/application.conf",
"chars": 141,
"preview": "play.http.filters=loader.Filters\nplay.crypto.secret=\"AkZXuiBfDLbr4;ZqVdfLZpHFZ:q4hryMV=0AGF25aYOE9`bWlRWbv/C=U=<>Kbxv\"\np"
},
{
"path": "sample-app-java/conf/routes",
"chars": 907,
"preview": "# Routes\n# This file defines all application routes (Higher priority routes first)\n# ~~~~\n\n# BigPipe examples\nGET "
},
{
"path": "sample-app-java/test/com/ybrikman/ping/PingJavaTestComponents.scala",
"chars": 1124,
"preview": "package com.ybrikman.ping\n\nimport akka.actor.ActorSystem\nimport play.api.libs.ws.WSClient\nimport play.api.mvc.{RequestHe"
},
{
"path": "sample-app-java/test/com/ybrikman/ping/Tests.scala",
"chars": 249,
"preview": "package com.ybrikman.ping\n\nclass DedupeSpec extends BaseDedupeSpec with PingJavaTestComponents\n\nclass BigPipeSpec extend"
},
{
"path": "sample-app-scala/app/controllers/Deduping.scala",
"chars": 1593,
"preview": "package controllers\n\nimport data.{UrlAndId, ServiceClient}\nimport play.api.mvc.{Action, Controller}\nimport play.api.libs"
},
{
"path": "sample-app-scala/app/controllers/Mock.scala",
"chars": 448,
"preview": "package controllers\n\nimport play.api.mvc.{Controller, Action}\n\n/**\n * This controller is used as a mock remote endpoint "
},
{
"path": "sample-app-scala/app/controllers/MoreBigPipeExamples.scala",
"chars": 7060,
"preview": "package controllers\n\nimport com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions\nimport com.ybrikman.ping.scalaapi.big"
},
{
"path": "sample-app-scala/app/controllers/WithBigPipe.scala",
"chars": 2044,
"preview": "package controllers\n\nimport com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions\nimport com.ybrikman.ping.scalaapi.big"
},
{
"path": "sample-app-scala/app/controllers/WithoutBigPipe.scala",
"chars": 1352,
"preview": "package controllers\n\nimport data.FakeServiceClient\nimport play.api.mvc.{Action, Controller}\nimport play.api.libs.concurr"
},
{
"path": "sample-app-scala/app/data/ServiceClient.scala",
"chars": 450,
"preview": "package data\n\nimport com.ybrikman.ping.scalaapi.dedupe.DedupingCache\nimport play.api.libs.ws.{WSResponse, WSClient}\nimpo"
},
{
"path": "sample-app-scala/app/loader/PingApplicationLoader.scala",
"chars": 1777,
"preview": "package loader\n\nimport com.ybrikman.ping.scalaapi.dedupe.{CacheFilter, DedupingCache}\nimport controllers._\nimport data.{"
},
{
"path": "sample-app-scala/conf/application.conf",
"chars": 161,
"preview": "play.application.loader=loader.PingApplicationLoader\nplay.crypto.secret=\"AkZXuiBfDLbr4;ZqVdfLZpHFZ:q4hryMV=0AGF25aYOE9`b"
},
{
"path": "sample-app-scala/conf/routes",
"chars": 913,
"preview": "# Routes\n# This file defines all application routes (Higher priority routes first)\n# ~~~~\n\n# Examples without BigPipe\nGE"
},
{
"path": "sample-app-scala/test/com/ybrikman/ping/PingScalaTestComponents.scala",
"chars": 944,
"preview": "package com.ybrikman.ping\n\nimport loader.PingComponents\nimport play.api.routing.Router\nimport play.api.{Mode, Environmen"
},
{
"path": "sample-app-scala/test/com/ybrikman/ping/Tests.scala",
"chars": 251,
"preview": "package com.ybrikman.ping\n\nclass DedupeSpec extends BaseDedupeSpec with PingScalaTestComponents\n\nclass BigPipeSpec exten"
},
{
"path": "version.sbt",
"chars": 41,
"preview": "version in ThisBuild := \"0.0.14-SNAPSHOT\""
}
]
About this extraction
This page contains the full source code of the brikis98/ping-play GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 99 files (200.8 KB), approximately 54.7k tokens, and a symbol index with 143 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.