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

example/darkmode: add darkmode example #185

Open
wants to merge 1 commit into
base: main
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
18 changes: 18 additions & 0 deletions example/darkmode/controller/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package controller

import (
"net/http"
)

type Controller struct {
Writer http.ResponseWriter
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an over-simplification of a feature I recently fixed in Bud. See this PR for details.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting - so a controller (or its dependencies) may depend on http.ResponseWriter and *http.Request and they'll be injected in on a per-request basis?

That's a neat way of keeping the action methods limited to request params.

I wonder if this comes with extra allocation cost or dependency construction churn though - for example, if I'm modeling my database as a dependency I probably wouldn't want it reconnecting with every request. Or does it only reconstruct dependencies as needed?

Copy link
Contributor Author

@matthewmueller matthewmueller Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this comes with extra allocation cost or dependency construction churn though - for example, if I'm modeling my database as a dependency I probably wouldn't want it reconnecting with every request. Or does it only reconstruct dependencies as needed?

So the database client is able to be passed in once through all requests.

There's a concept called in the DI framework called hoisting that's able to externalize dependencies that don't depend on specific other dependencies. Hoisting just means, mark those dependencies as something you need to pass into the provider as well.

In the case of controllers, dependencies that depend on *http.Request or http.ResponseWriter are not hoistable, they need to be re-initialized every request. Every other dependency is.

It looks like this in the generated code:

// Handler function
func (i *IndexAction) handler(httpResponse http.ResponseWriter, httpRequest *http.Request) http.Handler {
        controller, err := loadController(
                i.Client,
                i.Logger, httpRequest, httpResponse,
        )
        if err != nil {
                return &response.Format{
                        JSON: response.Status(500).Set("Content-Type", "application/json").JSON(map[string]string{"error": err.Error()}),
                }
        }
        handler := controller.Index
        // Call the controller
        in0 := handler()

        // Respond
        return &response.Format{
                JSON: response.JSON(in0),
        }
}

// Generated by di
func loadController(dbClient *db.Client, logLogger *log.Logger, httpRequest *http.Request, httpResponseWriter http.ResponseWriter) (*controller.Controller, error) {
        sessionSession := session.New(logLogger, httpResponseWriter, httpRequest)
        controllerController := &controller.Controller{Session: sessionSession, DB: dbClient}
        return controllerController, nil
}

Where i.Client and i.Logger don't depend on the request, so they're passed in rather than initialized by the DI provider. Session on the other hand does depend on the request, so it's initialized per request.

There's definitely an allocation cost though. Even without hoisting, we load the controller every time. If you're doing something expensive in your controller.Load() that would slow things down per request. Thinking about it now, I could see that being quite surprising. Actually, this may point in favor of passing request-scoped dependencies in.

I was indeed trying to avoid mixing dependencies with the API signature, but I think DI is smart enough at this point to be able to filter those keys out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks for the explanation.

Actually, this may point in favor of passing request-scoped dependencies in.

That sounds nice. Do you mean something like this?:

func (c *Controller) Logout(session *Session) error {
  session.Logout()
}

To me this feels pretty intuitive since as you said they're 'request scoped' just like the params are, but I guess you'd need some way for Bud (and maybe the developer) to disambiguate dependencies from params. 🤔

Or maybe you don't? I may be off my rocker here but one way of looking at it is that params are a just another type of dependency that load their values from the *http.Request.

Whether that's a line you want to blur is another question though regarding UX.

Copy link
Contributor Author

@matthewmueller matthewmueller Jul 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe you don't? I may be off my rocker here but one way of looking at it is that params are a just another type of dependency that load their values from the *http.Request.

This was my thought process at the time, but I'm definitely leaning more towards something like what you wrote now:

func (c *Controller) Logout(session *Session) error {
  session.Logout()
}

Which makes more intuitive sense because the lifecycle is more clear and doesn't suffer controller.Load() being potentially expensive. Bigger fish to fry right now, but I'll get back to this one before 1.0!

Persisted here: #211

}

func (c *Controller) Index() {}

func (c *Controller) Create(theme string) {
http.SetCookie(c.Writer, &http.Cookie{
Name: "theme",
Value: theme,
})
}
2 changes: 2 additions & 0 deletions example/darkmode/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
bud/
11 changes: 11 additions & 0 deletions example/darkmode/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/livebud/bud/example/darkmode

go 1.18

require (
github.com/livebud/bud v0.0.0
)

replace (
github.com/livebud/bud => /Users/m/dev/src/github.com/livebud/bud
)
36 changes: 36 additions & 0 deletions example/darkmode/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/ajg/form v1.5.2-0.20200323032839-9aeb3cf462e1 h1:8Qzi+0Uch1VJvdrOhJ8U8FqoPLbUdETPgMqGJ6DSMSQ=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/evanw/esbuild v0.14.11 h1:bw50N4v70Dqf/B6Wn+3BM6BVttz4A6tHn8m8Ydj9vxk=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 h1:Uc+IZ7gYqAf/rSGFplbWBSHaGolEQlNLgMgSE3ccnIQ=
github.com/gitchander/permutation v0.0.0-20201214100618-1f3e7285f953 h1:+rJDfq6waeB1BncyEfuFL1N3U7t3aahrAjPqcKLpMys=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
github.com/matthewmueller/diff v0.0.0-20220104030700-cb2fe910d90c h1:yjGBNrCIE7IghJAwrFcyDzwzwJKf0oRPeOHx60wfkmA=
github.com/matthewmueller/gotext v0.0.0-20210424201144-265ed61725ac h1:SjopLdUF96kdJU8ynYmGVHoJmngpwFHRvR5p2plBXG4=
github.com/matthewmueller/text v0.0.0-20210424201111-ec1e4af8dfe8 h1:XTmVlF7P9bpSNkLFlxpNlhig0kaVJ5mO4D3yK2CYjmM=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/pointlander/compress v1.1.1-0.20190518213731-ff44bd196cc3 h1:hUmXhbljNFtrH5hzV9kiRoddZ5nfPTq3K0Sb2hYYiqE=
github.com/pointlander/jetset v1.0.1-0.20190518214125-eee7eff80bd4 h1:RHHRCZeaNyBXdYPMjZNH8/XHDBH38TZzw8izrW7dmBE=
github.com/pointlander/peg v1.0.1 h1:mgA/GQE8TeS9MdkU6Xn6iEzBmQUQCNuWD7rHCK6Mjs0=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw=
github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk=
go.kuoruan.net/v8go-polyfills v0.5.0 h1:wd2WxsFIXWK/FcrpITw6BOo8Rn24xMmd4qoHofgg8hc=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
rogchap.com/v8go v0.7.0 h1:kgjbiO4zE5itA962ze6Hqmbs4HgZbGzmueCXsZtremg=
src.techknowlogick.com/xgo v1.4.1-0.20220413212431-091a0a22b814 h1:/oIyHjKnlyQ3yFzxq7uin83l6h0sHXT7Z+9TpP9wr8s=
125 changes: 125 additions & 0 deletions example/darkmode/module/cookie/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Export all by default
* Based on: https://github.com/component/cookie
*/

export default { all, get, set }

/**
* Cookie type
*/

type Cookies = {
[cookie: string]: string
}

/**
* Set options
*/

type Options = {
maxage?: number
expires?: Date
domain?: string
path?: string
secure?: boolean
}

/**
* Set cookie `name` to `value`.
*/

function set(name: string, value: string | null, options?: Options) {
options = options || {}
var str = encode(name) + '=' + encode(String(value))

if (null == value) {
options.maxage = -1
}

if (options.maxage) {
options.expires = new Date(+new Date() + options.maxage)
}

if (options.path) str += '; path=' + options.path
if (options.domain) str += '; domain=' + options.domain
if (options.expires) str += '; expires=' + options.expires.toUTCString()
if (options.secure) str += '; secure'

document.cookie = str
}

/**
* Return all cookies.
*
* This is isomorphic and may be called
* from the server-side though it will
* return nothing.
*/

function all(): Cookies {
var str
try {
str = document.cookie
} catch (err) {
console.log(err)
return {}
}
return parse(str)
}

/**
* Get cookie `name`.
*/

function get(name: string): string | undefined {
return all()[name]
}

/**
* Parse cookie `str`.
*/

function parse(str: string): Cookies {
var obj = <Cookies>{}
var pairs = str.split(/ *; */)
for (var i = 0; i < pairs.length; ++i) {
var pair = pairs[i]
var eqidx = pair.indexOf('=')
if (eqidx === -1) {
eqidx = pair.length
}
var name = decode(pair.substr(0, eqidx))
// +1 because we don't want the =
var value = decode(pair.substr(eqidx + 1))
if (!name || !value) {
continue
}
obj[name] = value
}
return obj
}

/**
* Encode.
*/

function encode(value: string): string | undefined {
try {
return encodeURIComponent(value)
} catch (e) {
return
}
}

/**
* Decode.
*/

function decode(value: string): string | undefined {
try {
return decodeURIComponent(value)
} catch (e) {
return
}
}
134 changes: 134 additions & 0 deletions example/darkmode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions example/darkmode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "darkmode",
"private": true,
"dependencies": {
"livebud": "latest",
"svelte": "3.47.0"
}
}
36 changes: 36 additions & 0 deletions example/darkmode/view/index.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script>
import cookie from "../module/cookie"
// TODO: fix SSR flickering. cookie.get(...) uses document.cookie under the
// hood. `document.cookie` should be accessible by V8 on the server-side.
// We'll need to take care exposing APIs though. We shouldn't try to polyfill
// the DOM on the server.
const theme = cookie.get("theme")
function submit(e) {
this.form.submit()
}
</script>

<div class={theme || "light"}>
<h1>Change Theme</h1>
<form action="/" method="post">
<select name="theme" on:change={submit}>
<option value="light" selected={theme === "light"}>Light</option>
<option value="dark" selected={theme === "dark"}>Dark</option>
</select>
</form>
</div>

<style>
.light {
--var-color: black;
--var-bgcolor: whiteSmoke;
}
.dark {
--var-color: whiteSmoke;
--var-bgcolor: black;
}
h1 {
color: var(--var-color);
background: var(--var-bgcolor);
}
</style>