![]() * WiP initial setup, ServerSentEventGenerator class * WiP working merge_fragments in Rails * #merge_fragments and #merge_signals * Handle SSE vs Data* options * Test that #merge_fragments works with a #call(view_context:) interface * Test Dispatcher#stream * #remove_fragments * #remove_signals * #execute_script * execute_script with attributes Hash * Connection: keep-alive * Use 2 line-breaks as message end, plus last line's 1 line break (3 total) * Connection callbacks. #on_connect, #on_disconnect, #on_error * Dispatcher#signals * Omit retry if using default value (1000) * Omit defaults * Multiline scripts * Test Rack endpoint * Document test Rack endpoint * Add missing defaults * Spawn multiple streams in threads, client_disconnect and server_disconnect handlers * Move ThreadSpawner to configuration * Configure a RailsThreadSpawner when Rails detected * Move Railtie one dir up * Global error callback Datastar.config.on_error { |err| Sentry.notify(err) } * Catch exception from stream threads in main thread * Linearlize exception handling * Refactor dispatcher to handle single stream in main thread, multi streams in separate threads * spawner => executor. Rails Async executor using fibers. * Support Async for fiber-based concurrency * Finalize response for Rack and Rails * test Rack app * Threaded demo * Test Dispatcher#sse? Also do not check for SSE Accept on stream. Leave it up to the user. * Do not check Accept header in test app. Test scripts don't send it properly. * Document code * Example progress bar Rack app * README * Link to D* SSE docs * See examples * Document callbacks * List Ruby SDK in SDKs.md * Ruby struct in consts.go * Document running tasks with arguments via Docker * Code-gen Ruby constants from shared data via template * Make test rely on constants * Datastar.from_rack_env(env) => Datastar::Dispatcher * Ruby example snippets * #redirect(location) * Ruby snippet using #redirect(new_path) * Add X-Accel-Buffering: no header To disable response buffering by NGinx and other proxies. * Clarify linearisation of updates in Readme * Tidy-up progress example * Move examples to /examples/ruby * Document Rails and Phlex * Version 1.0.0.beta.1 * Version 1.0.0.beta.1 * Do not set Connection header if not HTTP/1.1 * Don't touch BUILDING.md docs in this PR * Remove Changelog for now * Sort Ruby alphabetically (just "ruby", not the entire line) * Add hello world example, remove progress bar one. * Add hello-world example to code-gen * Typos |
||
---|---|---|
.. | ||
clojure | ||
dotnet | ||
go | ||
java | ||
php | ||
python | ||
ruby | ||
rust | ||
test | ||
typescript | ||
zig | ||
README.md |
README.md
Architecture Decision Record: Datastar SDK
Summary
Datastar has had a few helper tools in the past for different languages. The SDK effort is to unify around the tooling needed for Hypermedia On Whatever your Like (HOWL) based UIs. Although Datastar the library can use any plugins the default bundle includes robust Server Sent Event (SSE) base approach. Most current languages and backend don't have great tooling around the style of delivering content to the frontend.
Decision
Provide an SDK in a language agnostic way, to that end
- Keep SDK as minimal as possible
- Allow per language/framework extended features to live in an SDK sugar version
Details
Assumptions
The core mechanics of Datastar's SSE support is
- Data gets sent to browser as SSE events.
- Data comes in via JSON from browser under a
datastar
namespace.
Library
[!WARNING] All naming conventions are shown using
Go
as the standard, thing may change per language norms but please keep as close as possible.
ServerSentEventGenerator
There must be a ServerSentEventGenerator
namespace. In Go this is implemented as a struct, but could be a class or even namespace in languages such as C.
Construction / Initialization
- There must be a way to create a new instance of this object based on the incoming
HTTP
Request and Response objects. - The
ServerSentEventGenerator
must use a response controller that has the following response headers set by defaultCache-Control = nocache
Content-Type = text/event-stream
Connection = keep-alive
only if a HTTP/1.1 connection is used (see spec)
- Then the created response should
flush
immediately to avoid timeouts while 0-♾️ events are created - Multiple calls using
ServerSentEventGenerator
should be single threaded to guarantee order. The Go implementation use a mutex to facilitate this behavior but might not be need in a some environments
ServerSentEventGenerator.send
ServerSentEventGenerator.send(
eventType: EventType,
dataLines: string[],
options?: {
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
All top level ServerSentEventGenerator
should use a unified sending function. This method should be private/protected
Args
EventType
An enum of Datastar supported events. Will be a string over the wire. Currently valid values are
Event | Description |
---|---|
datastar-merge-fragments | Merges HTML fragments into the DOM |
datastar-merge-signals | Merges signals into the signals |
datastar-remove-fragments | Removes HTML fragments from the DOM |
datastar-remove-signals | Removes signals from the signals |
datastar-execute-script | Executes JavaScript in the browser |
Options
eventId
(string) Each event may include aneventId
. This can be used by the backend to replay events. This is part of the SSE spec and is used to tell the browser how to handle the event. For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#idretryDuration
(duration) Each event may include aretryDuration
value. If one is not provided the SDK must default to1000
milliseconds. This is part of the SSE spec and is used to tell the browser how long to wait before reconnecting if the connection is lost. For more details see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry
Logic
When called the function must write to the response buffer the following in specified order. If any part of this process fails you must return/throw an error depending on language norms.
- Must write
event: EVENT_TYPE\n
whereEVENT_TYPE
is EventType. - If a user defined event ID is provided, the function must write
id: EVENT_ID\n
whereEVENT_ID
is the event ID. - Must write
retry: RETRY_DURATION\n
whereRETRY_DURATION
is the provided retry duration, unless the value is the default of1000
milliseconds. - For each string in the provided
dataLines
, you must writedata: DATA\n
whereDATA
is the provided string. - Must write a
\n\n
to complete the event per the SSE spec. - Afterward the writer should immediately flush. This can be confounded by other middlewares such as compression layers.
ServerSentEventGenerator.MergeFragments
ServerSentEventGenerator.MergeFragments(
fragments: string,
options?: {
selector?: string,
mergeMode?: FragmentMergeMode,
settleDuration?: durationInMilliseconds,
useViewTransition?: boolean,
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
Example Output
Minimal:
event: datastar-merge-fragments
data: fragments <div id="feed">
data: fragments <span>1</span>
data: fragments </div>
Maximal:
event: datastar-merge-fragments
id: 123
retry: 2000
data: selector #feed
data: settleDuration 10
data: useViewTransition true
data: fragments <div id="feed">
data: fragments <span>1</span>
data: fragments </div>
MergeFragments
is a helper function to send HTML fragments to the browser to be merged into the DOM.
Args
FragmentMergeMode
An enum of Datastar supported fragment merge modes. Will be a string over the wire Valid values should match the FragmentMergeMode and currently include
Mode | Description |
---|---|
morph | Use idiomorph to merge the fragment into the DOM |
inner | Replace the innerHTML of the selector with the fragment |
outer | Replace the outerHTML of the selector with the fragment |
prepend | Prepend the fragment to the selector |
append | Append the fragment to the selector |
before | Insert the fragment before the selector |
after | Insert the fragment after the selector |
upsertAttributes | Update the attributes of the selector with the fragment |
Options
selector
(string) The CSS selector to use to insert the fragments. If not provided or empty, Datastar will default to using theid
attribute of the fragment.mergeMode
(FragmentMergeMode) The mode to use when merging the fragment into the DOM. If not provided the Datastar client side will default tomorph
.settleDuration
is used to control the amount of time that a fragment should take before removing any CSS related to settling. It is used to allow for animations in the browser via the Datastar client. If provided the value must be a positive integer of the number of milliseconds to allow for settling. If none is provided, the default value of300
milliseconds will be used.useViewTransition
Whether to use view transitions, if not provided the Datastar client side will default tofalse
.
Logic
When called the function must call ServerSentEventGenerator.send
with the datastar-merge-fragments
event type.
- If
selector
is provided, the function must include the selector in the event data in the formatselector SELECTOR\n
, unless the selector is empty. - If
mergeMode
is provided, the function must include the merge mode in the event data in the formatmerge MERGE_MODE\n
, unless the value is the default ofmorph
. - If
settleDuration
is provided, the function must include the settle duration in the event data in the formatsettleDuration SETTLE_DURATION\n
, unless the value is the default of300
milliseconds. - If
useViewTransition
is provided, the function must include the view transition in the event data in the formatuseViewTransition USE_VIEW_TRANSITION\n
, unless the value is the default offalse
.USE_VIEW_TRANSITION
should betrue
orfalse
(string), depending on the value of theuseViewTransition
option. - The function must include the fragments in the event data, with each line prefixed with
fragments
. This should be output after all other event data.
ServerSentEventGenerator.RemoveFragments
ServerSentEventGenerator.RemoveFragments(
selector: string,
options?: {
settleDuration?: durationInMilliseconds,
useViewTransition?: boolean,
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
Example Output
Minimal:
event: datastar-remove-fragments
data: selector #target
Maximal:
event: datastar-remove-fragments
id: 123
retry: 2000
data: selector #target
data: settleDuration 200
data: useViewTransition true
RemoveFragments
is a helper function to send a selector to the browser to remove HTML fragments from the DOM.
Args
selector
is a CSS selector that represents the fragments to be removed from the DOM. The selector must be a valid CSS selector. The Datastar client side will use this selector to remove the fragment from the DOM.
Options
settleDuration
is used to control the amount of time that a fragment should take before removing any CSS related to settling. It is used to allow for animations in the browser via the Datastar client. If provided the value must be a positive integer of the number of milliseconds to allow for settling. If none is provided, the default value of300
milliseconds will be used.useViewTransition
Whether to use view transitions, if not provided the Datastar client side will default tofalse
.
Logic
- When called the function must call
ServerSentEventGenerator.send
with thedatastar-remove-fragments
event type. - The function must include the selector in the event data in the format
selector SELECTOR\n
. - If
settleDuration
is provided, the function must include the settle duration in the event data in the formatsettleDuration SETTLE_DURATION\n
, unless the value is the default of300
milliseconds. - If
useViewTransition
is provided, the function must include the view transition in the event data in the formatuseViewTransition USE_VIEW_TRANSITION\n
, unless the value is the default offalse
.USE_VIEW_TRANSITION
should betrue
orfalse
(string), depending on the value of theuseViewTransition
option.
ServerSentEventGenerator.MergeSignals
ServerSentEventGenerator.MergeSignals(
signals: string,
options ?: {
onlyIfMissing?: boolean,
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
Example Output
Minimal:
event: datastar-merge-signals
data: signals {"output":"Patched Output Test","show":true,"input":"Test","user":{"name":"","email":""}}
Maximal:
event: datastar-merge-signals
id: 123
retry: 2000
data: onlyIfMissing true
data: signals {"output":"Patched Output Test","show":true,"input":"Test","user":{"name":"","email":""}}
MergeSignals
is a helper function to send one or more signals to the browser to be merged into the signals.
Args
Data is a JavaScript object or JSON string that will be sent to the browser to update signals in the signals. The data must evaluate to a valid JavaScript. It will be converted to signals by the Datastar client side.
Options
onlyIfMissing
(boolean) Whether to merge the signal only if it does not already exist. If not provided, the Datastar client side will default tofalse
, which will cause the data to be merged into the signals.
Logic
When called the function must call ServerSentEventGenerator.send
with the datastar-merge-signals
event type.
- If
onlyIfMissing
is provided, the function must include it in the event data in the formatonlyIfMissing ONLY_IF_MISSING\n
, unless the value is the default offalse
.ONLY_IF_MISSING
should betrue
orfalse
(string), depending on the value of theonlyIfMissing
option. - The function must include the signals in the event data, with each line prefixed with
signals
. This should be output after all other event data.
ServerSentEventGenerator.RemoveSignals
ServerSentEventGenerator.RemoveSignals(
paths: string[],
options?: {
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
Example Output
Minimal:
event: datastar-remove-signals
data: paths user.name
data: paths user.email
Maximal:
event: datastar-remove-signals
id: 123
retry: 2000
data: paths user.name
data: paths user.email
RemoveSignals
is a helper function to send signals to the browser to be removed from the signals.
Args
paths
is a list of strings that represent the signal paths to be removed from the signals. The paths must be valid .
delimited paths to signals within the signals. The Datastar client side will use these paths to remove the data from the signals.
Logic
When called the function must call ServerSentEventGenerator.send
with the datastar-remove-signals
event type.
- The function must include the paths in the event data, with each line prefixed with
paths
. Space-separated paths such aspaths foo.bar baz.qux.hello world
should be allowed.
ServerSentEventGenerator.ExecuteScript
ServerSentEventGenerator.ExecuteScript(
script: string,
options?: {
autoRemove?: boolean,
attributes?: string,
eventId?: string,
retryDuration?: durationInMilliseconds
}
)
Example Output
Minimal:
event: datastar-execute-script
data: script window.location = "https://data-star.dev"
Maximal:
event: datastar-execute-script
id: 123
retry: 2000
data: autoRemove false
data: attributes type text/javascript
data: script window.location = "https://data-star.dev"
Args
script
is a string that represents the JavaScript to be executed by the browser.
Options
autoRemove
Whether to remove the script after execution, if not provided the Datastar client side will default totrue
.attributes
A line separated list of attributes to add to thescript
element, if not provided the Datastar client side will default totype module
. Each item in the array should be a string in the formatkey value
.
Logic
When called the function must call ServerSentEventGenerator.send
with the datastar-execute-script
event type.
- If
autoRemove
is provided, the function must include the auto remove script value in the event data in the formatautoRemove AUTO_REMOVE\n
, unless the value is the default oftrue
. - If
attributes
is provided, the function must include the attributes in the event data, with each line prefixed withattributes
, unless the attributes value is the default oftype module
. - The function must include the script in the event data, with each line prefixed with
script
. This should be output after all other event data.
ReadSignals(r *http.Request, signals any) error
ReadSignals
is a helper function to parse incoming data from the browser. It should take the incoming request and convert into an object that can be used by the backend.
Args
r
(http.Request) The incoming request object from the browser. This object must be a valid Request object per the language specifics.signals
(any) The signals object that will the incoming data will be unmarshalled into. The exact function signature will depend on the language specifics.
Logic
- The function must parse the incoming HTTP request
- If the incoming method is
GET
, the function must parse the query string'sdatastar
key and treat it as a URL encoded JSON string. - Otherwise, the function must parse the body of the request as a JSON encoded string.
- If the incoming data is not valid JSON, the function must return an error.
- If the incoming method is