Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local Server Push + push intermediates without routes #17

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 56 additions & 27 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"path"
"sort"
"time"

Expand All @@ -31,13 +32,22 @@ type (
modTime time.Time
}

linkHeader struct {
file string
as string
}

// TODO: add all headers including cache-control and
// service worker so no regex matching is needed at runtime

// PushHeaders are the link headers to send for a route
PushHeaders map[string][]string
PushHeaders map[string][]linkHeader
)

func (l linkHeader) String() string {
return fmt.Sprintf("<%s>; rel=preload; as=%s", l.file, l.as)
}

var files = make(map[string]*file)

func loadBuilds(config *ProjectConfig, root http.Dir, routes Routes, version string, createTemplate createTemplateFn) builds {
Expand Down Expand Up @@ -152,42 +162,61 @@ func newBuild(config *ProjectConfig, configOrder int, name string, requirements
pushHeaders := PushHeaders{}
prefix := version + name + "/"

for path, fragment := range routes {
for file, assets := range pushManifest {
headers := []linkHeader{}

for p, asset := range assets {
link := linkHeader{
file: path.Join(prefix, p),
as: asset.Type,
}
headers = append(headers, link)
}

pushHeaders[path.Join(prefix, file)] = headers
}

for route, fragment := range routes {
set := map[string]struct{}{}
headers := []string{
fmt.Sprintf("<%s%s>; rel=preload; as=%s", prefix, "bower_components/webcomponentsjs/webcomponents-loader.js", "script"),
fmt.Sprintf("<%s%s>; rel=preload; as=%s", prefix, config.Shell, "document"),
headers := []linkHeader{
{
file: path.Join(prefix, "bower_components/webcomponentsjs/webcomponents-loader.js"),
as: "script",
},
{
file: path.Join(prefix, config.Shell),
as: "document",
},
}
set[headers[0]] = struct{}{}
set[headers[1]] = struct{}{}
for path, asset := range pushManifest[config.Shell] {
link := fmt.Sprintf("<%s%s>; rel=preload; as=%s", prefix, path, asset.Type)
if _, found := set[link]; !found {
set[link] = struct{}{}
set[headers[0].String()] = struct{}{}
set[headers[1].String()] = struct{}{}
for p, asset := range pushManifest[config.Shell] {
link := linkHeader{
file: path.Join(prefix, p),
as: asset.Type,
}
if _, found := set[link.String()]; !found {
set[link.String()] = struct{}{}
headers = append(headers, link)
}
}

headers = append(headers, fmt.Sprintf("<%s%s>; rel=preload; as=%s", prefix, fragment, "document"))
for path, asset := range pushManifest[fragment] {
link := fmt.Sprintf("<%s%s>; rel=preload; as=%s", prefix, path, asset.Type)
if _, found := set[link]; !found {
set[link] = struct{}{}
headers = append(headers, linkHeader{
file: path.Join(prefix, fragment),
as: "document",
})
for p, asset := range pushManifest[fragment] {
link := linkHeader{
file: path.Join(prefix, p),
as: asset.Type,
}
if _, found := set[link.String()]; !found {
set[link.String()] = struct{}{}
headers = append(headers, link)
}
}

pushHeaders[path] = headers
}

// update paths to account for the build folder name
manifest := Manifest{}
for path, assets := range pushManifest {
adjusted := make(map[string]AssetOpt, len(assets))
for assetPath, asset := range assets {
adjusted[prefix+assetPath] = asset
}
manifest[prefix+path] = adjusted
pushHeaders[route] = headers
}

build := build{
Expand Down
2 changes: 1 addition & 1 deletion capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (c capability) String() string {
return strings.Join(val, ", ")
}

func (p *prpl) browserCapabilities(userAgentString string) capability {
func (p *PRPL) browserCapabilities(userAgentString string) capability {
client := p.parser.Parse(userAgentString)

predicate, ok := browserPredicates[client.UserAgent.Family]
Expand Down
36 changes: 23 additions & 13 deletions prpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type (
// prpl is an instance of the prpl-server service
prpl struct {
PRPL struct {
http.Handler
parser *uaparser.Parser
config *ProjectConfig
Expand All @@ -18,15 +18,16 @@ type (
version string
staticHandlers map[string]http.Handler
createTemplate createTemplateFn
shouldPush func(r *http.Request) bool
}

// optionFn provides functional option configuration
optionFn func(*prpl) error
optionFn func(*PRPL) error
)

// New creates a new prpl instance
func New(options ...optionFn) (*prpl, error) {
p := prpl{
func New(options ...optionFn) (*PRPL, error) {
p := PRPL{
parser: uaparser.NewFromSaved(),
root: http.Dir("."),
version: "/static/",
Expand Down Expand Up @@ -60,39 +61,39 @@ func New(options ...optionFn) (*prpl, error) {

// WithVersion sets the version prefix
func WithVersion(version string) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.version = "/" + version + "/"
return nil
}
}

// WithRoutes sets the route -> fragment mapping
func WithRoutes(routes Routes) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.routes = routes
return nil
}
}

// WithRoot sets the root directory
func WithRoot(root http.Dir) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.root = root
return nil
}
}

// WithConfig sets the project configuration
func WithConfig(config *ProjectConfig) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.config = config
return nil
}
}

// WithConfigFile loads the project configuration
func WithConfigFile(filename string) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
config, err := ConfigFromFile(filename)
if err != nil {
return err
Expand All @@ -105,7 +106,7 @@ func WithConfigFile(filename string) optionFn {
// WithUAParserFile allows the uaparser configuration
// to be overriden from the inbuilt settings
func WithUAParserFile(regexFile string) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
parser, err := uaparser.New(regexFile)
if err != nil {
return err
Expand All @@ -118,7 +119,7 @@ func WithUAParserFile(regexFile string) optionFn {
// WithUAParserBytes allows the uaparser configuration
// to be overriden from the inbuilt settings
func WithUAParserBytes(data []byte) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
parser, err := uaparser.NewFromBytes(data)
if err != nil {
return err
Expand All @@ -133,7 +134,7 @@ func WithUAParserBytes(data []byte) optionFn {
// the manifest.json file per tenant or to serve specific
// images based on host headers etc ...
func WithStaticHandler(path string, handler http.Handler) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.staticHandlers[path] = handler
return nil
}
Expand All @@ -143,8 +144,17 @@ func WithStaticHandler(path string, handler http.Handler) optionFn {
// into a template so that the output can be transformed if
// required
func WithRouteTemplate(factory createTemplateFn) optionFn {
return func(p *prpl) error {
return func(p *PRPL) error {
p.createTemplate = factory
return nil
}
}

// WithShouldPush specifies when the server should do a direct HTTP server push
// instead of just setting the server push Link header.
func WithShouldPush(shouldPush func(*http.Request) bool) optionFn {
return func(p *PRPL) error {
p.shouldPush = shouldPush
return nil
}
}
75 changes: 45 additions & 30 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import (
"net/http"
)

func (p *prpl) createHandler() http.Handler {
var (
CacheImmutable = "public, max-age=31536000, immutable"
CacheNever = "public, max-age=0"
CacheNeverPrivate = "private, max-age=0"
)

func (p *PRPL) createHandler() http.Handler {
m := http.NewServeMux()

for _, build := range p.builds {
Expand All @@ -24,7 +30,7 @@ func (p *prpl) createHandler() http.Handler {
return m
}

func (p *prpl) routeHandler(w http.ResponseWriter, r *http.Request) {
func (p *PRPL) routeHandler(w http.ResponseWriter, r *http.Request) {
capabilities := p.browserCapabilities(r.UserAgent())
build := p.builds.findBuild(capabilities)
if build == nil {
Expand All @@ -33,59 +39,68 @@ func (p *prpl) routeHandler(w http.ResponseWriter, r *http.Request) {
}

h := w.Header()
h.Set("Cache-Control", "public, max-age=0")
build.addHeaders(w, h, r.URL.Path)
h.Set("Cache-Control", CacheNever)
build.addHeaders(p, w, r)
build.template.Render(w, r)
}

func (p *prpl) staticHandler(next http.Handler) http.Handler {
func (p *PRPL) staticHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// TODO: Service worker location should be configurable.
h := w.Header()
if strings.HasSuffix(r.URL.Path, "service-worker.js") {
h.Set("Service-Worker-Allowed", "/")
h.Set("Cache-Control", "private, max-age=0")
h.Set("Cache-Control", CacheNeverPrivate)
} else {
h.Set("Cache-Control", "public, max-age=31536000, immutable")
h.Set("Cache-Control", CacheImmutable)
}

capabilities := p.browserCapabilities(r.UserAgent())
build := p.builds.findBuild(capabilities)
if build == nil {
http.Error(w, "This browser is not supported", http.StatusInternalServerError)
return
}

build.addHeaders(p, w, r)

file, found := files[r.URL.Path]
if !found {
next.ServeHTTP(w, r)
return
}

// TODO: if using original prpl-server-node strategy
// add the push headers for *this* push-manifest entry
// build.addHeaders(w, h, r.URL.Path)

content := bytes.NewReader(file.data)
http.ServeContent(w, r, r.URL.Path, file.modTime, content)
}

return http.HandlerFunc(fn)
}

func (b *build) addHeaders(w http.ResponseWriter, header http.Header, filename string) {
if links, ok := b.pushHeaders[filename]; ok {
// TODO: use actual push if server supports it
// need to add content type to push header info
// if pusher, ok := w.(http.Pusher); ok {
/*
for _, url := range headers {
pusher.Push(url, &http.PushOptions{
Header: http.Header{
"Cache-Control": []string{"public, max-age=31536000, immutable"},
"Content-Type": []string{"TODO: file content type here"},
},
})
}
*/
// } else {
// otherwise hope there is a proxy that will do it for us
func (b *build) addHeaders(p *PRPL, w http.ResponseWriter, r *http.Request) {
header := w.Header()
filename := r.URL.Path

links, ok := b.pushHeaders[filename]
if !ok {
links, ok = b.pushHeaders[p.version+filename]
if !ok {
return
}
}

if pusher, ok := w.(http.Pusher); ok && p.shouldPush != nil && p.shouldPush(r) {
for _, link := range links {
header.Add("Link", link)
pusher.Push(link.file, &http.PushOptions{
Header: http.Header{
"Cache-Control": []string{CacheImmutable},
//"Content-Type": []string{"TODO: file content type here"},
},
})
}
// }
}

for _, link := range links {
header.Add("Link", link.String())
}
}