jaws

package module
v0.2.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2022 License: MIT Imports: 21 Imported by: 2

README

build coverage goreport

JaWS

Javascript and WebSockets used to create responsive webpages.

HTTP request flow and associating the WebSocket

When a new HTTP request is received, create a JaWS Request using the JaWS object's NewRequest() method, and then use the Request's HeadHTML() method to get the HTML code needed in the HEAD section of the HTML page.

When the client has finished loading the document and parsed the scripts, the JaWS Javascript will request a WebSocket connection on /jaws/*, with the * being the hexadecimal string of the Request.JawsKey value.

On receiving the WebSocket HTTP request, decode the key parameter from the URL and call the JaWS object's UseRequest() method to retrieve the Request created in the first step. Then call it's ServeHTTP() method to start up the WebSocket and begin processing Javascript events and DOM updates.

Routing

JaWS doesn't enforce any particular router, but it does require several endpoints to be registered in whichever router you choose to use. We do provide a helper function for Echo with jawsecho.Setup().

  • /jaws/jaws.*.js

    The exact URL is the return value of jaws.JavascriptPath(). It must return the client-side Javascript, the uncompressed contents of which can be had with jaws.JavascriptText(), or a gzipped version with jaws.JavascriptGZip().

    The response should be cached indefinitely.

  • /jaws/[0-9a-fA-F]+

    The WebSocket endpoint. The trailing hex string must be decoded and then the matching JaWS Request retrieved using the JaWS object's UseRequest() method.

    If the Request is not found, return a 404 Not Found, otherwise call the Request ServeHTTP() method to start the WebSocket and begin processing events and updates.

  • /jaws/ping

    This endpoint is called by the Javascript while waiting for the server to come online. This is done in order to not spam the WebSocket endpoint with connection requests, and browsers are better at handling XHR requests failing.

    If you don't have a JaWS object, or if it's completion channel is closed (see Jaws.Done()), return 503 Service Unavailable. If you're ready to serve requests, return 200 OK.

    The response should not be cached.

Registering HTML entities and Javascript events

The application registers the HTML entity id's it wants to interact with per request, usually while rendering the HTML template. If a HTML entity id is not registered in a Request, JaWS will not forward events from it, nor perform DOM manipulations for it.

Dynamic updates of a HTML entity is done using the different methods on the JaWS object and Request object. If the JaWS object is used to update a HTML entity, all Requests will receive the update request. If the Request object's methods are used, the update is forwarded to to all other Requests.

A note on the Context

The Request object embeds a context.Context inside it's struct, contrary to recommended Go practice.

The reason is that there is no unbroken call chain from the time the Request object is created when the initial HTTP request comes in and when it's requested during the Javascript WebSocket HTTP request.

Security of the WebSocket callback

Each JaWS request gets a unique 63-bit random value assigned to it when you create the Request object. This value is written to the HTML output so it can be read by the Javascript, and used to construct the WebSocket callback URL.

Once the WebSocket call comes in, the value is consumed by that request, and is no longer valid until, theoretically, another Request gets the same random value. And that's fine, since JaWS guarantees that no two Requests waiting for WebSocket calls can have the same value at the same time.

In addition to this, Requests that are not claimed by a WebSocket call get cleaned up at regular intervals. By default an unclaimed Request is removed after 10 seconds.

In order to guess (and thus hijack) a WebSocket you'd have to make on the order of 2^62 requests before the genuine request comes in, or 10 seconds pass assuming you can reliably prevent the genuine WebSocket request.

Dependencies

We try to minimize dependencies outside of the standard library.

Depends on https://github.com/nhooyr/websocket for WebSocket functionality. Depends on https://github.com/matryer/is for tests.

Documentation

Overview

Package jaws provides a mechanism to create dynamic webpages using Javascript and WebSockets.

It integrates well with Go's html/template package, but can be used without it. It can be used with any router that supports the standard ServeHTTP interface.

It comes with a small package 'jawsecho' that integrates with Echo and also doubles as an example for integration with other routers.

Index

Constants

View Source
const BootstrapDefaultCDN = "https://cdn.jsdelivr.net/npm/bootstrap"
View Source
const BootstrapDefaultVersion = "5.2.2"
View Source
const ISO8601 = "2006-01-02"

Variables

View Source
var FuncMap = template.FuncMap{

	"int": CastInt,

	"float64": CastFloat64,
}

Functions

func CastFloat64

func CastFloat64(i int) float64

func CastInt

func CastInt(f float64) int

func HeadHTML

func HeadHTML(jawsKey uint64, extra ...string) template.HTML

HeadHTML returns the HTML code to load the required CSS and Javascript libraries along with any `*.js“ and `*.css` URL's given in `extra`. Place the returned HTML code in the HEAD section of the document.

func JavascriptGZip

func JavascriptGZip() []byte

JavascriptGZip returns the embedded Javascript library GZipped.

func JavascriptPath

func JavascriptPath() string

JavascriptPath returns the path for the embedded JaWS Javascript library.

func JavascriptText

func JavascriptText() []byte

JavascriptText returns the source code for the client-side JaWS Javascript library.

func JawsKeyString

func JawsKeyString(jawsKey uint64) string

func UseBootstrap added in v0.2.12

func UseBootstrap(bc *BootstrapConfig)

UseBootstrap allows customizing the Bootstrap configuration. If not called explicitly, the first JaWS instance will call it. This is not thread-safe, so must be called before using a JaWS instance. Passing nil sets the default configuration.

Types

type BootstrapConfig added in v0.2.12

type BootstrapConfig struct {
	Version string
	CDN     string
	// contains filtered or unexported fields
}

type ClickFn

type ClickFn func(rq *Request) error

type ConnectFn

type ConnectFn func(rq *Request) error

ConnectFn can be used to interact with a Request before message processing starts. Returning an error causes the Request to abort, and the WebSocket connection to close.

type EventFn

type EventFn func(rq *Request, id, evt, val string) error

EventFn is the signature of a event handling function to be called when JaWS receives an event message from the Javascript via the WebSocket connection.

type InputBoolFn

type InputBoolFn func(rq *Request, val bool) error

type InputDateFn

type InputDateFn func(rq *Request, val time.Time) error

type InputFloatFn

type InputFloatFn func(rq *Request, val float64) error

type InputTextFn

type InputTextFn func(rq *Request, val string) error

type Jaws

type Jaws struct {
	Logger *log.Logger // If not nil, send debug info and errors here
	// contains filtered or unexported fields
}

func New

func New() (jw *Jaws)

New returns a new JaWS object that must be closed using Close(). This is expected to be created once per HTTP server and handles publishing HTML changes across all connections.

func NewWithDone

func NewWithDone(doneCh <-chan struct{}) *Jaws

NewWithDone returns a new JaWS object using the given completion channel. This is expected to be created once per HTTP server and handles publishing HTML changes across all connections.

func (*Jaws) Alert

func (jw *Jaws) Alert(lvl, msg string)

Alert sends an alert to all Requests. The lvl argument should be one of Bootstraps alert levels: primary, secondary, success, danger, warning, info, light or dark.

func (*Jaws) Append

func (jw *Jaws) Append(parentId, html string)

Append calls the Javascript 'appendChild()' method on the given element on all Requests.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) Broadcast

func (jw *Jaws) Broadcast(msg *Message)

Broadcast sends a message to all Requests.

func (*Jaws) Close

func (jw *Jaws) Close()

Close frees resources associated with the JaWS object, and closes the completion channel if the JaWS was created with New(). Once the completion channel is closed, broadcasts and sends are discarded. Subsequent calls to Close() have no effect.

func (*Jaws) Done

func (jw *Jaws) Done() <-chan struct{}

Done returns the completion channel.

func (*Jaws) Insert

func (jw *Jaws) Insert(parentId, where, html string)

Insert calls the Javascript 'insertBefore()' method on the given element on all Requests. The position parameter 'where' may be either a HTML ID, an child index or the text 'null'.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) Log

func (jw *Jaws) Log(err error) error

Log sends an error to the Logger set in the Jaws. Has no effect if the err is nil or the Logger is nil. Returns err.

func (*Jaws) MakeID

func (jw *Jaws) MakeID() string

MakeID returns a string in the form 'jaws.X' where X is a string unique within the Jaws lifetime.

func (*Jaws) MustLog added in v0.1.1

func (jw *Jaws) MustLog(err error)

MustLog sends an error to the Logger set in the Jaws or panics with the given error if no Logger is set. Has no effect if the err is nil.

func (*Jaws) NewRequest

func (jw *Jaws) NewRequest(ctx context.Context, remoteAddr string) (rq *Request)

NewRequest returns a new JaWS request. Call this as soon as you start processing a HTML request, and store the returned Request pointer so it can be used while constructing the HTML response in order to register the HTML entity id's you use in the response, and use it's Key attribute when sending the Javascript portion of the reply with GetBodyFooter.

Don't use the http.Request's Context, as that will expire before the WebSocket call comes in.

func (*Jaws) Pending

func (jw *Jaws) Pending() (n int)

Count returns the number of requests waiting for their WebSocket callbacks.

func (*Jaws) Redirect

func (jw *Jaws) Redirect(url string)

Redirect requests all Requests to navigate to the given URL.

func (*Jaws) Reload

func (jw *Jaws) Reload()

Reload requests all Requests to reload their current page.

func (*Jaws) Remove

func (jw *Jaws) Remove(id string)

Remove removes the HTML element with the given ID on all Requests.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) RemoveAttr

func (jw *Jaws) RemoveAttr(id, attr string)

RemoveAttr removes a given attribute from the HTML id for all Requests.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) Replace

func (jw *Jaws) Replace(id, where, html string)

Replace calls the Javascript 'replaceChild()' method on the given element on all Requests. The position parameter 'where' may be either a HTML ID or an index.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) Serve

func (jw *Jaws) Serve()

Serve calls ServeWithTimeout(time.Second * 10).

func (*Jaws) ServeWithTimeout

func (jw *Jaws) ServeWithTimeout(requestTimeout time.Duration)

ServeWithTimeout begins processing requests with the given timeout. It is intended to run on it's own goroutine. It returns when the completion channel is closed.

func (*Jaws) SetAttr

func (jw *Jaws) SetAttr(id, attr, val string)

SetAttr sends an HTML id and new attribute value to all Requests. If the value is an empty string, a value-less attribute will be added (such as "disabled")

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) SetInner

func (jw *Jaws) SetInner(id string, innerHtml string)

SetInner sends an HTML id and new inner HTML to all Requests.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) SetValue

func (jw *Jaws) SetValue(id, val string)

SetValue sends an HTML id and new input value to all Requests.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Jaws) Trigger

func (jw *Jaws) Trigger(id, val string)

Trigger invokes the event handler for the given ID with a 'trigger' event on all Requests.

func (*Jaws) UseRequest

func (jw *Jaws) UseRequest(jawsKey uint64, remoteAddr string) (rq *Request)

UseRequest removes the JaWS request with the given key from the request map if it exists and the remoteAddr matches, and if so returns the Request.

Call it when receiving the WebSocket connection on '/jaws/:key' to get the associated Request, and then call it's ServeHTTP method to process the WebSocket messages.

Returns nil if the key was not found, in which case you should return a HTTP "404 Not Found" status.

type Message

type Message struct {
	Elem string // HTML element id or command (e.g. ' alert' or 'myButtonId')
	What string // what to change or do, (e.g. 'inner')
	Data string // data (e.g. inner HTML content)
	// contains filtered or unexported fields
}

Message contains the elements of a message to be sent to Requests.

func (*Message) Format

func (msg *Message) Format() string

Format returns the Message in the form it's expected by the Javascript.

func (*Message) String

func (msg *Message) String() string

String returns the Message in a form suitable for debug output.

type NamedBool

type NamedBool struct {
	Value   string
	Text    string
	Checked bool
}

NamedBool stores the data required to support HTML 'select' elements and sets of HTML radio buttons.

func (*NamedBool) String

func (nb *NamedBool) String() string

type NamedBoolArray

type NamedBoolArray []*NamedBool

NamedBoolArray is an array of pointers to NamedBool.

func NewNamedBoolArray

func NewNamedBoolArray() *NamedBoolArray

NewNamedBoolArray allocates an array of *NamedBool and returns a pointer to it.

func (*NamedBoolArray) Add

func (nb *NamedBoolArray) Add(val, text string)

Add creates a new NamedBool and appends it's pointer to the NamedBoolArray.

func (*NamedBoolArray) Check

func (nb *NamedBoolArray) Check(val string)

Check sets all NamedBool's Checked to true in the NamedBoolArray that have the given Value.

func (*NamedBoolArray) Set

func (nb *NamedBoolArray) Set(val string, state bool)

Set sets all NamedBool's Checked to state in the NamedBoolArray that have the given Value.

func (*NamedBoolArray) String

func (nb *NamedBoolArray) String() string

type Request

type Request struct {
	Jaws      *Jaws     // the JaWS instance the Request belongs to
	JawsKey   uint64    // a random number used in the WebSocket URI to identify this Request
	ConnectFn ConnectFn // a ConnectFn to call before starting message processing for the Request
	Created   time.Time // when the Request was created, used for automatic cleanup
	Started   bool      // set to true after UseRequest() has been called
	// contains filtered or unexported fields
}

Request maintains the state for a JaWS WebSocket connection, and handles processing of events and broadcasts.

Note that we have to store the context inside the struct because there is no call chain between the Request being created and it being used once the WebSocket is created.

func (*Request) A

func (rq *Request) A(id, inner string, fn ClickFn, attrs string) template.HTML

func (*Request) Alert

func (rq *Request) Alert(lvl, msg string)

Alert attempts to show an alert message on the current request webpage if it has an HTML element with the id 'jaws-alert'. The lvl argument should be one of Bootstraps alert levels: primary, secondary, success, danger, warning, info, light or dark.

The default JaWS javascript only supports Bootstrap.js dismissable alerts.

func (*Request) AlertError

func (rq *Request) AlertError(err error)

AlertError calls Alert if the given error is not nil.

func (*Request) Broadcast

func (rq *Request) Broadcast(msg *Message)

Broadcast sends a broadcast to all Requests except the current one.

func (*Request) Button

func (rq *Request) Button(id, txt string, fn ClickFn, attrs string) template.HTML

func (*Request) Checkbox

func (rq *Request) Checkbox(id string, val bool, fn InputBoolFn, attrs string) template.HTML

func (*Request) Context

func (rq *Request) Context() (ctx context.Context)

Context returns the context passed to NewRequest()

func (*Request) Date

func (rq *Request) Date(id string, val time.Time, fn InputDateFn, attrs string) template.HTML

func (*Request) Div

func (rq *Request) Div(id, inner string, fn ClickFn, attrs string) template.HTML

func (*Request) GetEventFn

func (rq *Request) GetEventFn(id string) (fn EventFn, ok bool)

GetEventFn checks if a given HTML element is registered and returns the it's event function (or nil) along with a boolean indicating if it's a registered HTML id.

func (*Request) HeadHTML

func (rq *Request) HeadHTML() template.HTML

HeadHTML returns the HTML code needed to write in the HTML page's HEAD section.

func (*Request) JawsKeyString

func (rq *Request) JawsKeyString() string

func (*Request) Li

func (rq *Request) Li(id, inner string, fn ClickFn, attrs string) template.HTML

func (*Request) Number added in v0.2.9

func (rq *Request) Number(id string, val float64, fn InputFloatFn, attrs string) template.HTML

func (*Request) OnClick

func (rq *Request) OnClick(id string, fn ClickFn) error

OnClick registers a HTML id and a function to be called when it's click event fires. Returns a nil error so it can be used inside templates.

func (*Request) OnEvent

func (rq *Request) OnEvent(id string, fn EventFn) error

OnEvent calls SetEventFn. Returns a nil error so it can be used inside templates.

func (*Request) OnInput

func (rq *Request) OnInput(id string, fn InputTextFn) error

OnInput registers a HTML id and a function to be called when it's input event fires. Returns a nil error so it can be used inside templates.

func (*Request) Password added in v0.2.6

func (rq *Request) Password(id string, fn InputTextFn, attrs string) template.HTML

func (*Request) Radio

func (rq *Request) Radio(id string, val bool, fn InputBoolFn, attrs string) template.HTML

func (*Request) Range

func (rq *Request) Range(id string, val float64, fn InputFloatFn, attrs string) template.HTML

func (*Request) Redirect

func (rq *Request) Redirect(url string)

Redirect requests the current Request to navigate to the given URL.

func (*Request) Register

func (rq *Request) Register(id string) string

Register calls RegisterEventFn(id, nil). Useful in template constructs like:

<div id="{{$.Register `foo`}}">

func (*Request) RegisterEventFn

func (rq *Request) RegisterEventFn(id string, fn EventFn) string

RegisterEventFn records the given HTML element ID as a valid target for dynamic updates using the given event function (which may be nil).

If the id argument is an empty string, a unique ID will be generated.

If fn argument is nil, a pre-existing event function won't be overwritten.

All ID's in a HTML DOM tree must be unique, and submitting a duplicate id with a non-nil fn before UseRequest() have been called will cause a panic. Once UseRequest has been called, you are allowed to call this function with already registered ID's since otherwise updating inner HTML using the element functions (e.g. Request.Text) would fail.

Returns the (possibly generated) id.

func (*Request) RemoveAttr

func (rq *Request) RemoveAttr(id, attr string)

RemoveAttr removes a given attribute from the HTML id for the current Request only.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) Select

func (rq *Request) Select(id string, val *NamedBoolArray, fn InputTextFn, attrs string) template.HTML

func (*Request) Send

func (rq *Request) Send(msg *Message) bool

Send queues up a message for sending to the current Request only. Returns true if the message was successfully queued for sending.

func (*Request) ServeHTTP

func (rq *Request) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.HanderFunc

func (*Request) SetAttr

func (rq *Request) SetAttr(id, attr, val string)

SetAttr sets an attribute on the HTML element on the current Request only. If the value is an empty string, a value-less attribute will be added (such as "disabled").

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) SetBoolValue

func (rq *Request) SetBoolValue(id string, val bool)

SetBoolValue sends an HTML id and new input value to all Requests except this one.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) SetDateValue

func (rq *Request) SetDateValue(id string, val time.Time)

SetDateValue sends an HTML id and new input value to all Requests except this one.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) SetEventFn

func (rq *Request) SetEventFn(id string, fn EventFn)

SetEventFn sets the event function for the given HTML ID to be the given function. Passing nil for the function is legal, and has the effect of ensuring the ID can be the target of DOM updates but not to send Javascript events. Note that you can only have one event function per ID.

func (*Request) SetFloatValue

func (rq *Request) SetFloatValue(id string, val float64)

SetFloatValue sends an HTML id and new input value to all Requests except this one.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) SetInner

func (rq *Request) SetInner(id string, innerHtml string)

SetInner sends an HTML id and new inner HTML to all Requests except this one.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) SetTextValue

func (rq *Request) SetTextValue(id, val string)

SetTextValue sends an HTML id and new input value to all Requests except this one.

Only the requests that have registered the ID (either with Register or OnEvent) will be sent the message.

func (*Request) Span

func (rq *Request) Span(id, inner string, fn ClickFn, attrs string) template.HTML

func (*Request) String

func (rq *Request) String() string

func (*Request) Td

func (rq *Request) Td(id, inner string, fn ClickFn, attrs string) template.HTML

func (*Request) Text

func (rq *Request) Text(id, val string, fn InputTextFn, attrs string) template.HTML

func (*Request) Trigger

func (rq *Request) Trigger(id, val string)

Trigger invokes the event handler for the given ID with a 'trigger' event on all Requests except this one.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL