{"id":26675663,"url":"https://github.com/linkdata/jaws","last_synced_at":"2025-07-25T22:37:46.232Z","repository":{"id":49387299,"uuid":"517630021","full_name":"linkdata/jaws","owner":"linkdata","description":"Javascript and WebSockets enabling dynamic webpages","archived":false,"fork":false,"pushed_at":"2025-04-01T05:40:53.000Z","size":1055,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-12T08:11:38.638Z","etag":null,"topics":["dynamic-web","dynamic-webpages","echo-framework","go","golang","websocket","websockets"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/linkdata.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-25T11:04:31.000Z","updated_at":"2025-04-01T05:40:56.000Z","dependencies_parsed_at":"2024-04-29T06:28:54.651Z","dependency_job_id":"4584bc95-0ae3-4425-909e-5813e9e21b28","html_url":"https://github.com/linkdata/jaws","commit_stats":null,"previous_names":[],"tags_count":170,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linkdata%2Fjaws","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linkdata%2Fjaws/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linkdata%2Fjaws/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/linkdata%2Fjaws/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/linkdata","download_url":"https://codeload.github.com/linkdata/jaws/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248537140,"owners_count":21120711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dynamic-web","dynamic-webpages","echo-framework","go","golang","websocket","websockets"],"created_at":"2025-03-26T03:19:12.260Z","updated_at":"2025-04-12T08:11:49.586Z","avatar_url":"https://github.com/linkdata.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![build](https://github.com/linkdata/jaws/actions/workflows/go.yml/badge.svg)](https://github.com/linkdata/jaws/actions/workflows/go.yml)\n[![coverage](https://coveralls.io/repos/github/linkdata/jaws/badge.svg?branch=main)](https://coveralls.io/github/linkdata/jaws?branch=main)\n[![goreport](https://goreportcard.com/badge/github.com/linkdata/jaws)](https://goreportcard.com/report/github.com/linkdata/jaws)\n[![Docs](https://godoc.org/github.com/linkdata/jaws?status.svg)](https://godoc.org/github.com/linkdata/jaws)\n\n# JaWS\n\nJavascript and WebSockets used to create responsive webpages.\n\n* Moves web application state fully to the server.\n* Does not trust the web browser or the Javascript.\n* Binds application data to UI elements using user-definable 'tags'.\n\nThere is a [demo application](https://github.com/linkdata/jawsdemo)\nwith plenty of comments to use as a tutorial.\n\n## Usage\n\n```go\nimport (\n\t\"html/template\"\n\t\"log/slog\"\n\t\"net/http\"\n\n\t\"github.com/linkdata/jaws\"\n)\n\nconst indexhtml = `\n\u003chtml\u003e\n\u003chead\u003e{{$.HeadHTML}}\u003c/head\u003e\n\u003cbody\u003e{{$.Range .Dot}}\u003c/body\u003e\n\u003c/html\u003e\n`\n\nfunc main() {\n\tjw := jaws.New()           // create a default JaWS instance\n\tdefer jw.Close()           // ensure we clean up\n\tjw.Logger = slog.Default() // optionally set the logger to use\n\n\t// parse our template and inform JaWS about it\n\ttemplates := template.Must(template.New(\"index\").Parse(indexhtml))\n\tjw.AddTemplateLookuper(templates)\n\n\tgo jw.Serve()                             // start the JaWS processing loop\n\thttp.DefaultServeMux.Handle(\"/jaws/\", jw) // ensure the JaWS routes are handled\n\n\tvar f jaws.Float // somewhere to store the slider data\n\thttp.DefaultServeMux.Handle(\"/\", jw.Handler(\"index\", \u0026f))\n\tslog.Error(http.ListenAndServe(\"localhost:8080\", nil).Error())\n}\n```\n\n### Creating HTML entities\n\nWhen JawsRender() is called for a UI object, it can call\nNewElement() to create new Elements while writing their initial\nHTML code to the web page. Each Element is a unique instance\nof a UI object bound to a specific Request, and will have a\nunique HTML id.\n\nIf a HTML entity is not registered in a Request, JaWS will not\nforward events from it, nor perform DOM manipulations for it.\n\nDynamic updates of HTML entities is done using the different methods on\nthe Element object when the JawsUpdate() method is called.\n\n### Javascript events\n\nSupported Javascript events are sent to the server and\nare handled by the Element's UI type. If that didn't handle the event,\nany extra objects added to the Element are invoked (in order) until one\nhandles the event. If none handle the event, it is ignored.\n\nThe generic event handler is `JawsEvent`. An event handler should\nreturn `ErrEventUnhandled` if it didn't handle the event or wants\nto pass it to the next handler.\n\n* `onclick` invokes `JawsClick` if present, otherwise `JawsEvent` with `what.Click`\n* `oninput` invokes `JawsEvent` with `what.Input`\n\n## Technical notes\n\n### HTTP request flow and associating the WebSocket\n\nWhen a new HTTP request is received, create a JaWS Request using the JaWS\nobject's `NewRequest()` method, and then use the Request's `HeadHTML()` \nmethod to get the HTML code needed in the HEAD section of the HTML page.\n\nWhen the client has finished loading the document and parsed the scripts,\nthe JaWS Javascript will request a WebSocket connection on `/jaws/*`, \nwith the `*` being the encoded Request.JawsKey value.\n\nOn receiving the WebSocket HTTP request, decode the key parameter from \nthe URL and call the JaWS object's `UseRequest()` method to retrieve the\nRequest created in the first step. Then call it's `ServeHTTP()` method to\nstart up the WebSocket and begin processing Javascript events and DOM updates.\n\n### Routing\n\nJaWS doesn't enforce any particular router, but it does require several\nendpoints to be registered in whichever router you choose to use. All of\nthe endpoints start with \"/jaws/\", and `Jaws.ServeHTTP()` will handle all\nof them.\n\n* `/jaws/jaws.*.js`\n\n  The exact URL is the value of `jaws.JavascriptPath`. It must return\n  the client-side Javascript, the uncompressed contents of which can be had with\n  `jaws.JavascriptText`, or a gzipped version with `jaws.JavascriptGZip`.\n\n  The response should be cached indefinitely.\n\n* `/jaws/[0-9a-z]+`\n\n  The WebSocket endpoint. The trailing string must be decoded using \n  `jaws.JawsKeyValue()` and then the matching JaWS Request retrieved\n  using the JaWS object's `UseRequest()` method.\n\n  If the Request is not found, return a **404 Not Found**, otherwise \n  call the Request `ServeHTTP()` method to start the WebSocket and begin \n  processing events and updates.\n\n* `/jaws/.ping`\n\n  This endpoint is called by the Javascript while waiting for the server to\n  come online. This is done in order to not spam the WebSocket endpoint with\n  connection requests, and browsers are better at handling XHR requests failing.\n\n  If you don't have a JaWS object, or if it's completion channel is closed (see\n  `Jaws.Done()`), return **503 Service Unavailable**. If you're ready to serve\n  requests, return **204 No Content**.\n  \n  The response should not be cached.\n\nHandling the routes with the standard library's `http.DefaultServeMux`:\n\n```go\njw := jaws.New()\ndefer jw.Close()\ngo jw.Serve()\nhttp.DefaultServeMux.Handle(\"/jaws/\", jw)\n```\n\nHandling the routes with [Echo](https://echo.labstack.com/):\n\n```go\njw := jaws.New()\ndefer jw.Close()\ngo jw.Serve()\nrouter := echo.New()\nrouter.GET(\"/jaws/*\", func(c echo.Context) error {\n  jw.ServeHTTP(c.Response().Writer, c.Request())\n  return nil\n})\n```\n\n### HTML rendering\n\nHTML output elements (e.g. `jaws.RequestWriter.Div()`) require a `jaws.HTMLGetter` or something that can\nbe made into one using `jaws.MakeHTMLGetter()`.\n\nIn order of precedence, this can be:\n* `jaws.HTMLGetter`: `JawsGetHTML(*Element) template.HTML` to be used as-is.\n* `jaws.Getter[string]`: `JawsGet(*Element) string` that will be escaped using `html.EscapeString`.\n* `jaws.AnyGetter`: `JawsGetAny(*Element) any` that will be rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.\n* `fmt.Stringer`: `String() string` that will be escaped using `html.EscapeString`.\n* a static `template.HTML` or `string` to be used as-is with no HTML escaping.\n* everything else is rendered using `fmt.Sprint()` and escaped using `html.EscapeString`.\n\nYou can use `jaws.Bind().FormatHTML()`, `jaws.HTMLGetterFunc()` or `jaws.StringGetterFunc()` to build a custom renderer\nfor trivial rendering tasks, or define a custom type implementing `HTMLGetter`.\n\n### Data binding\n\nHTML input elements (e.g. `jaws.RequestWriter.Range()`) require bi-directional data flow between the server and the browser.\nThe first argument to these is usually a `Setter[T]` where `T` is one of `string`, `float64`, `bool` or `time.Time`. It can\nalso be a `Getter[T]`, in which case the HTML element should be made read-only.\n\nSince all data access need to be protected with locks, you will usually use `jaws.Bind()` to create a `jaws.Binder[T]`\nthat combines a (RW)Locker and a pointer to a value of type `T`. It also allows you to add chained setters,\ngetters and on-success handlers.\n\n### Session handling\n\nJaWS has non-persistent session handling integrated. Sessions won't \nbe persisted across restarts and must have an expiry time. A new\nsession is created with `EnsureSession()` and sending it's `Cookie()`\nto the client browser.\n\nWhen subsequent Requests are created with `NewRequest()`, if the\nHTTP request has the cookie set and comes from the correct IP,\nthe new Request will have access to that Session.\n\nSession key-value pairs can be accessed using `Request.Set()` and\n`Request.Get()`, or directly using a `Session` object. It's safe to\ndo this if there is no session; `Get()` will return nil, and `Set()`\nwill be a no-op.\n\nSessions are bound to the client IP. Attempting to access an existing \nsession from a new IP will fail.\n\nNo data is stored in the client browser except the randomly generated \nsession cookie. You can set the cookie name in `Jaws.CookieName`, the\ndefault is `jaws`.\n\n### A note on the Context\n\nThe Request object embeds a context.Context inside it's struct,\ncontrary to recommended Go practice.\n\nThe reason is that there is no unbroken call chain from the time the Request\nobject is created when the initial HTTP request comes in and when it's \nrequested during the Javascript WebSocket HTTP request.\n\n### Security of the WebSocket callback\n\nEach JaWS request gets a unique 64-bit random value assigned to it when you \ncreate the Request object. This value is written to the HTML output so it\ncan be read by the Javascript, and used to construct the WebSocket callback\nURL.\n\nOnce the WebSocket call comes in, the value is consumed by that request,\nand is no longer valid until, theoretically, another Request gets the same\nrandom value. And that's fine, since JaWS guarantees that no two Requests\nwaiting for WebSocket calls can have the same value at the same time.\n\nIn addition to this, Requests that are not claimed by a WebSocket call get\ncleaned up at regular intervals. By default an unclaimed Request is \nremoved after 10 seconds.\n\nIn order to guess (and thus hijack) a WebSocket you'd have to make on the\norder of 2^63 requests before the genuine request comes in, or 10 seconds\npass assuming you can reliably prevent the genuine WebSocket request.\n\n### Dependencies\n\nWe try to minimize dependencies outside of the standard library.\n\n* Depends on https://github.com/coder/websocket for WebSocket functionality.\n* Depends on https://github.com/linkdata/deadlock if race detection is enabled.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinkdata%2Fjaws","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flinkdata%2Fjaws","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flinkdata%2Fjaws/lists"}