Current section

Files

Jump to
lustre src lustre@server_component.erl
Raw

src/lustre@server_component.erl

-module(lustre@server_component).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-define(FILEPATH, "src/lustre/server_component.gleam").
-export([element/2, script/0, route/1, method/1, include/2, subject/1, pid/1, register_subject/1, deregister_subject/1, register_callback/1, deregister_callback/1, emit/2, select/1, runtime_message_decoder/0, client_message_to_json/1]).
-export_type([transport_method/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Server components are an advanced feature that allows you to run components\n"
" or full Lustre applications on the server. Updates are broadcast to a small\n"
" (10kb!) client runtime that patches the DOM and events are sent back to the\n"
" server component in real-time.\n"
"\n"
" ```text\n"
" -- SERVER -----------------------------------------------------------------\n"
"\n"
" Msg Element(Msg)\n"
" +--------+ v +----------------+ v +------+\n"
" | | <-------------- | | <-------------- | |\n"
" | update | | Lustre runtime | | view |\n"
" | | --------------> | | --------------> | |\n"
" +--------+ ^ +----------------+ ^ +------+\n"
" #(model, Effect(msg)) | ^ Model\n"
" | |\n"
" | |\n"
" DOM patches | | DOM events\n"
" | |\n"
" v |\n"
" +-----------------------+\n"
" | |\n"
" | Your WebSocket server |\n"
" | |\n"
" +-----------------------+\n"
" | ^\n"
" | |\n"
" DOM patches | | DOM events\n"
" | |\n"
" v |\n"
" -- BROWSER ----------------------------------------------------------------\n"
" | ^\n"
" | |\n"
" DOM patches | | DOM events\n"
" | |\n"
" v |\n"
" +----------------+\n"
" | |\n"
" | Client runtime |\n"
" | |\n"
" +----------------+\n"
" ```\n"
"\n"
" > **Note**: Lustre's server component runtime is separate from your application's\n"
" > WebSocket server. You're free to bring your own stack, connect multiple\n"
" > clients to the same Lustre instance, or keep the application alive even when\n"
" > no clients are connected.\n"
"\n"
" Lustre server components run next to the rest of your backend code, your\n"
" services, your database, etc. Real-time applications like chat services, games,\n"
" or components that can benefit from direct access to your backend services\n"
" like an admin dashboard or data table are excellent candidates for server\n"
" components.\n"
"\n"
" ## Examples\n"
"\n"
" Server components are a new feature in Lustre and we're still working on the\n"
" best ways to use them and show them off. Here are a few examples we've\n"
" developed so far:\n"
"\n"
" - [Basic setup](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/01-basic-setup)\n"
"\n"
" - [Custom attributes and events](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/02-attributes-and-events)\n"
"\n"
" - [Decoding DOM events](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/03-event-include)\n"
"\n"
" - [Connecting more than one client](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/04-multiple-clients)\n"
"\n"
" ## Getting help\n"
"\n"
" If you're having trouble with Lustre or not sure what the right way to do\n"
" something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).\n"
" You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).\n"
"\n"
).
-type transport_method() :: web_socket | server_sent_events | polling.
-file("src/lustre/server_component.gleam", 141).
?DOC(
" Render the server component custom element. This element acts as the thin\n"
" client runtime for a server component running remotely. There are a handful\n"
" of attributes you should provide to configure the client runtime:\n"
"\n"
" - [`route`](#route) is the URL your server component should connect to. This\n"
" **must** be provided before the client runtime will do anything. The route\n"
" can be a relative URL, in which case it will be resolved against the current\n"
" page URL.\n"
"\n"
" - [`method`](#method) is the transport method the client runtime should use.\n"
" This defaults to `WebSocket` enabling duplex communication between the client\n"
" and server runtime. Other options include `ServerSentEvents` and `Polling`\n"
" which are unidirectional transports.\n"
"\n"
" > **Note**: the server component runtime bundle must be included and sent to\n"
" > the client for this to work correctly. You can do this by including the\n"
" > JavaScript bundle found in Lustre's `priv/static` directory or by inlining\n"
" > the script source directly with the [`script`](#script) element below.\n"
).
-spec element(
list(lustre@vdom@vattr:attribute(VHD)),
list(lustre@vdom@vnode:element(VHD))
) -> lustre@vdom@vnode:element(VHD).
element(Attributes, Children) ->
lustre@element:element(
<<"lustre-server-component"/utf8>>,
Attributes,
Children
).
-file("src/lustre/server_component.gleam", 153).
?DOC(
" Inline the server component client runtime as a `<script>` tag. Where possible\n"
" you should prefer serving the pre-built client runtime from Lustre's `priv/static`\n"
" directory, but this inline script can be useful for development or scenarios\n"
" where you don't control the HTML document.\n"
).
-spec script() -> lustre@vdom@vnode:element(any()).
script() ->
lustre@element@html:script(
[lustre@attribute:type_(<<"module"/utf8>>)],
<<"var qt=5,re=Math.pow(2,qt),Tn=re-1,Nn=re/2,Mn=re/4;var De=[\" \",\" \",`\\n`,\"\\v\",\"\\f\",\"\\r\",\"\\x85\",\"\\u2028\",\"\\u2029\"].join(\"\"),vr=new RegExp(`^[${De}]*`),kr=new RegExp(`[${De}]*$`);var g=()=>globalThis?.document,le=\"http://www.w3.org/1999/xhtml\",ae=1,ce=3,fe=11,Ge=!!globalThis.HTMLElement?.prototype?.moveBefore;var nt=0;var rt=1;var it=2;var st=0;var ut=1;var ot=2;var lt=3;var at=`\\n`,ct=\" \";var xt=new WeakMap;async function $t(i){let e=[];for(let n of g().querySelectorAll(\"link[rel=stylesheet], style\"))n.sheet||e.push(new Promise((r,s)=>{n.addEventListener(\"load\",r),n.addEventListener(\"error\",s)}));if(await Promise.allSettled(e),!i.host.isConnected)return[];i.adoptedStyleSheets=i.host.getRootNode().adoptedStyleSheets;let t=[];for(let n of g().styleSheets)try{i.adoptedStyleSheets.push(n)}catch{try{let r=xt.get(n);if(!r){r=new CSSStyleSheet;for(let s of n.cssRules)r.insertRule(s.cssText,r.cssRules.length);xt.set(n,r)}i.adoptedStyleSheets.push(r)}catch{let r=n.ownerNode.cloneNode();i.prepend(r),t.push(r)}}return t}var wt=0;var bt=1;var yt=2;var he=3;var gt=4;var me=5;var xe=6;var $e=7;var F=class{offset=0;#r=null;#e=()=>{};#t=!1;#n=!1;constructor(e,t,{useServerEvents:n=!1,exposeKeys:r=!1}={}){this.#r=e,this.#e=t,this.#t=n,this.#n=r}mount(e){Q(this.#r,this.#p(this.#r,e))}#i=[];push(e){let t=this.offset;t&&(w(e.changes,n=>{switch(n.kind){case xe:case he:n.before=(n.before|0)+t;break;case $e:case me:n.from=(n.from|0)+t;break}}),w(e.children,n=>{n.index=(n.index|0)+t})),this.#i.push({node:this.#r,patch:e}),this.#s()}#s(){let e=this;for(;e.#i.length;){let{node:t,patch:n}=e.#i.pop();w(n.changes,u=>{switch(u.kind){case xe:e.#u(t,u.children,u.before);break;case he:e.#c(t,u.key,u.before,u.count);break;case gt:e.#l(t,u.key,u.count);break;case $e:e.#o(t,u.from,u.count);break;case me:e.#a(t,u.from,u.count,u.with);break;case wt:e.#d(t,u.content);break;case bt:e.#_(t,u.inner_html);break;case yt:e.#x(t,u.added,u.removed);break}}),n.removed&&e.#o(t,t.childNodes.length-n.removed,n.removed);let r=-1,s=null;w(n.children,u=>{let o=u.index|0,b=s&&r-o===1?s.previousSibling:R(t,o);e.#i.push({node:b,patch:u}),s=b,r=o})}}#u(e,t,n){let r=jt();w(t,s=>{let u=this.#p(e,s);Q(r,u)}),we(e,r,R(e,n))}#c(e,t,n,r){let s=Et(e,t),u=R(e,n);for(let o=0;o<r&&s!==null;++o){let b=s.nextSibling;Ge?e.moveBefore(s,u):we(e,s,u),s=b}}#l(e,t,n){this.#f(e,Et(e,t),n)}#o(e,t,n){this.#f(e,R(e,t),n)}#f(e,t,n){for(;n-- >0&&t!==null;){let r=t.nextSibling,s=t[p].key;s&&e[p].keyedChildren.delete(s);for(let[u,{timeout:o}]of t[p].debouncers??[])clearTimeout(o);e.removeChild(t),t=r}}#a(e,t,n,r){this.#o(e,t,n);let s=this.#p(e,r);we(e,s,R(e,t))}#d(e,t){e.data=t??\"\"}#_(e,t){e.innerHTML=t??\"\"}#x(e,t,n){w(n,r=>{let s=r.name;e[p].handlers.has(s)?(e.removeEventListener(s,be),e[p].handlers.delete(s),e[p].throttles.has(s)&&e[p].throttles.delete(s),e[p].debouncers.has(s)&&(clearTimeout(e[p].debouncers.get(s).timeout),e[p].debouncers.delete(s))):(e.removeAttribute(s),At[s]?.removed?.(e,s))}),w(t,r=>{this.#m(e,r)})}#p(e,t){switch(t.kind){case ut:{let n=vt(e,t);return this.#h(n,t),this.#u(n,t.children,0),n}case ot:return kt(e,t);case st:{let n=jt(),r=kt(e,t);return Q(n,r),w(t.children,s=>{Q(n,this.#p(e,s))}),n}case lt:{let n=vt(e,t);return this.#h(n,t),this.#_(n,t.inner_html),n}}}#h(e,{key:t,attributes:n}){this.#n&&t&&e.setAttribute(\"data-lustre-key\",t),w(n,r=>this.#m(e,r))}#m(e,t){let{debouncers:n,handlers:r,throttles:s}=e[p],{kind:u,name:o,value:b,prevent_default:Nt,stop_propagation:Mt,immediate:te,include:It,debounce:je,throttle:Ee}=t;switch(u){case nt:{let l=b??\"\";if(o===\"virtual:defaultValue\"){e.defaultValue=l;return}l!==e.getAttribute(o)&&e.setAttribute(o,l),At[o]?.added?.(e,b);break}case rt:e[o]=b;break;case it:{if(r.has(o)&&e.removeEventListener(o,be),e.addEventListener(o,be,{passive:!t.prevent_default}),Ee>0){let l=s.get(o)??{};l.delay=Ee,s.set(o,l)}else s.delete(o);if(je>0){let l=n.get(o)??{};l.delay=je,n.set(o,l)}else clearTimeout(n.get(o)?.timeout),n.delete(o);r.set(o,l=>{Nt&&l.preventDefault(),Mt&&l.stopPropagation();let v=l.type,$=\"\",A=l.currentTarget;for(;A!==this.#r;){let C=A[p].key,z=A.parentNode;if(C)$=`${ct}${C}${$}`;else{let Ut=z.childNodes,Se=[].indexOf.call(Ut,A);z===this.#r&&(Se-=this.offset),$=`${at}${Se}${$}`}A=z}$=$.slice(1);let ne=this.#t?jn(l,It??[]):l,k=s.get(v);if(k){let C=Date.now(),z=k.last||0;C>z+k.delay&&(k.last=C,k.lastEvent=l,this.#e(ne,$,v,te))}let B=n.get(v);B&&(clearTimeout(B.timeout),B.timeout=setTimeout(()=>{l!==s.get(v)?.lastEvent&&this.#e(ne,$,v,te)},B.delay)),!k&&!B&&this.#e(ne,$,v,te)});break}}}},w=(i,e)=>{if(Array.isArray(i))for(let t=0;t<i.length;t++)e(i[t]);else if(i)for(i;i.tail;i=i.tail)e(i.head)},Q=(i,e)=>i.appendChild(e),we=(i,e,t)=>i.insertBefore(e,t??null),vt=(i,{key:e,tag:t,namespace:n})=>{let r=g().createElementNS(n||le,t);return P(i,r,e),r},kt=(i,{key:e,content:t})=>{let n=g().createTextNode(t??\"\");return P(i,n,e),n},jt=()=>g().createDocumentFragment(),R=(i,e)=>i.childNodes[e|0],p=Symbol(\"lustre\"),P=(i,e,t=\"\")=>{switch(e.nodeType){case ae:case fe:e[p]={key:t,keyedChildren:new Map,handlers:new Map,throttles:new Map,debouncers:new Map};break;case ce:e[p]={key:t};break}i&&t&&i[p].keyedChildren.set(t,new WeakRef(e))};var Et=(i,e)=>i[p].keyedChildren.get(e).deref(),be=i=>{let t=i.currentTarget[p].handlers.get(i.type);i.type===\"submit\"&&(i.detail??={},i.detail.formData=[...new FormData(i.target).entries()]),t(i)},jn=(i,e=[])=>{let t={};(i.type===\"input\"||i.type===\"change\")&&e.push(\"target.value\"),i.type===\"submit\"&&e.push(\"detail.formData\");for(let n of e){let r=n.split(\".\");for(let s=0,u=i,o=t;s<r.length;s++){if(s===r.length-1){o[r[s]]=u[r[s]];break}o=o[r[s]]??={},u=u[r[s]]}}return t},St=i=>({added(e){e[i]=!0},removed(e){e[i]=!1}}),En=i=>({added(e,t){e[i]=t}}),At={checked:St(\"checked\"),selected:St(\"selected\"),value:En(\"value\"),autofocus:{added(i){queueMicrotask(()=>i.focus?.())}},autoplay:{added(i){try{i.play?.()}catch(e){console.error(e)}}}};var Bt=0;var Ct=1;var zt=2;var Z=0;var Ot=1;var Tt=2;var ee=3;var ye=class extends HTMLElement{static get observedAttributes(){return[\"route\",\"method\"]}#r;#e=\"ws\";#t=null;#n=null;#i=[];#s;#u=new Set;#c=new Set;#l=!1;#o=[];#f=new MutationObserver(e=>{let t=[];for(let n of e){if(n.type!==\"attributes\")continue;let r=n.attributeName;(!this.#l||this.#u.has(r))&&t.push([r,this.getAttribute(r)])}if(t.length===1){let[n,r]=t[0];this.#n?.send({kind:Z,name:n,value:r})}else t.length?this.#n?.send({kind:ee,messages:t.map(([n,r])=>({kind:Z,name:n,value:r}))}):this.#o.push(...t)});constructor(){super(),this.internals=this.attachInternals(),this.#f.observe(this,{attributes:!0})}connectedCallback(){this.#e=this.getAttribute(\"method\")||\"ws\";for(let t of this.attributes)this.#o.push([t.name,t.value]);let e=this.getAttribute(\"route\");e&&(this.#t=new URL(e,location.href),this.#a())}attributeChangedCallback(e,t,n){switch(e){case(t!==n&&\"route\"):{this.#t=new URL(n,location.href),this.#a();return}case\"method\":{let r=n.toLowerCase();if(r==this.#e)return;[\"ws\",\"sse\",\"polling\"].includes(r)&&(this.#e=r,this.#e==\"ws\"&&(this.#t.protocol==\"https:\"&&(this.#t.protocol=\"wss:\"),this.#t.protocol==\"http:\"&&(this.#t.protocol=\"ws:\")),this.#a());return}}}async messageReceivedCallback(e){switch(e.kind){case Bt:{for(this.#r??=this.attachShadow({mode:e.open_shadow_root?\"open\":\"closed\"});this.#r.firstChild;)this.#r.firstChild.remove();P(null,this.#r),this.#s=new F(this.#r,(n,r,s)=>{this.#n?.send({kind:Ot,path:r,name:s,event:n})},{useServerEvents:!0}),this.#u=new Set(e.observed_attributes);let t=this.#o.filter(([n])=>this.#u.has(n));t.length&&this.#n.send({kind:ee,messages:t.map(([n,r])=>({kind:Z,name:n,value:r}))}),this.#o=[],this.#c=new Set(e.observed_properties);for(let n of this.#c)Object.defineProperty(this,n,{get(){return this[`_${n}`]},set(r){this[`_${n}`]=r,this.#n?.send({kind:Tt,name:n,value:r})}});e.will_adopt_styles&&await this.#d(),this.#s.mount(e.vdom),this.dispatchEvent(new CustomEvent(\"lustre:mount\"));break}case Ct:{this.#s.push(e.patch);break}case zt:{this.dispatchEvent(new CustomEvent(e.name,{detail:e.data}));break}}}#a(){if(!this.#t||!this.#e)return;this.#n&&this.#n.close();let r={onConnect:()=>{this.#l=!0,this.dispatchEvent(new CustomEvent(\"lustre:connect\"),{detail:{route:this.#t,method:this.#e}})},onMessage:s=>{this.messageReceivedCallback(s)},onClose:()=>{this.#l=!1,this.dispatchEvent(new CustomEvent(\"lustre:close\"),{detail:{route:this.#t,method:this.#e}})}};switch(this.#e){case\"ws\":this.#n=new ge(this.#t,r);break;case\"sse\":this.#n=new ve(this.#t,r);break;case\"polling\":this.#n=new ke(this.#t,r);break}}async#d(){for(;this.#i.length;)this.#i.pop().remove(),this.#r.firstChild.remove();this.#i=await $t(this.#r),this.#s.offset=this.#i.length}},ge=class{#r;#e;#t=!1;#n=[];#i;#s;#u;constructor(e,{onConnect:t,onMessage:n,onClose:r}){this.#r=e,this.#e=new WebSocket(this.#r),this.#i=t,this.#s=n,this.#u=r,this.#e.onopen=()=>{this.#i()},this.#e.onmessage=({data:s})=>{try{this.#s(JSON.parse(s))}finally{this.#n.length?this.#e.send(JSON.stringify({kind:ee,messages:this.#n})):this.#t=!1,this.#n=[]}},this.#e.onclose=()=>{this.#u()}}send(e){if(this.#t||this.#e.readyState!==WebSocket.OPEN){this.#n.push(e);return}else this.#e.send(JSON.stringify(e)),this.#t=!0}close(){this.#e.close()}},ve=class{#r;#e;#t;#n;#i;constructor(e,{onConnect:t,onMessage:n,onClose:r}){this.#r=e,this.#e=new EventSource(this.#r),this.#t=t,this.#n=n,this.#i=r,this.#e.onopen=()=>{this.#t()},this.#e.onmessage=({data:s})=>{try{this.#n(JSON.parse(s))}catch{}}}send(e){}close(){this.#e.close(),this.#i()}},ke=class{#r;#e;#t;#n;#i;#s;constructor(e,{onConnect:t,onMessage:n,onClose:r,...s}){this.#r=e,this.#n=t,this.#i=n,this.#s=r,this.#e=s.interval??5e3,this.#u().finally(()=>{this.#n(),this.#t=setInterval(()=>this.#u(),this.#e)})}async send(e){}close(){clearInterval(this.#t),this.#s()}#u(){return fetch(this.#r).then(e=>e.json()).then(this.#i).catch(console.error)}};customElements.define(\"lustre-server-component\",ye);export{ye as ServerComponent};"/utf8>>
).
-file("src/lustre/server_component.gleam", 168).
?DOC(
" The `route` attribute tells the client runtime what route it should use to\n"
" set up the WebSocket connection to the server. Whenever this attribute is\n"
" changed (by a clientside Lustre app, for example), the client runtime will\n"
" destroy the current connection and set up a new one.\n"
).
-spec route(binary()) -> lustre@vdom@vattr:attribute(any()).
route(Path) ->
lustre@attribute:attribute(<<"route"/utf8>>, Path).
-file("src/lustre/server_component.gleam", 174).
?DOC("\n").
-spec method(transport_method()) -> lustre@vdom@vattr:attribute(any()).
method(Value) ->
lustre@attribute:attribute(<<"method"/utf8>>, case Value of
web_socket ->
<<"ws"/utf8>>;
server_sent_events ->
<<"sse"/utf8>>;
polling ->
<<"polling"/utf8>>
end).
-file("src/lustre/server_component.gleam", 211).
?DOC(
" Properties of a JavaScript event object are typically not serialisable. This\n"
" means if we want to send them to the server we need to make a copy of any\n"
" fields we want to decode first.\n"
"\n"
" This attribute tells Lustre what properties to include from an event. Properties\n"
" can come from nested fields by using dot notation. For example, you could include\n"
" the\n"
" `id` of the target `element` by passing `[\"target.id\"]`.\n"
"\n"
" ```gleam\n"
" import gleam/dynamic/decode\n"
" import lustre/element.{type Element}\n"
" import lustre/element/html\n"
" import lustre/event\n"
" import lustre/server_component\n"
"\n"
" pub fn custom_button(on_click: fn(String) -> msg) -> Element(msg) {\n"
" let handler = fn(event) {\n"
" use id <- decode.at([\"target\", \"id\"], decode.string)\n"
" decode.success(on_click(id))\n"
" }\n"
"\n"
" html.button(\n"
" [server_component.include([\"target.id\"]), event.on(\"click\", handler)],\n"
" [html.text(\"Click me!\")],\n"
" )\n"
" }\n"
" ```\n"
).
-spec include(lustre@vdom@vattr:attribute(VHP), list(binary())) -> lustre@vdom@vattr:attribute(VHP).
include(Event, Properties) ->
case Event of
{event, _, _, _, _, _, _, _, _, _} ->
_record = Event,
{event,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
Properties,
erlang:element(6, _record),
erlang:element(7, _record),
erlang:element(8, _record),
erlang:element(9, _record),
erlang:element(10, _record)};
_ ->
Event
end.
-file("src/lustre/server_component.gleam", 231).
?DOC(
" Recover the `Subject` of the server component runtime so that it can be used\n"
" in supervision trees or passed to other processes. If you want to hand out\n"
" different `Subject`s to send messages to your application, take a look at the\n"
" [`select`](#select) effect.\n"
"\n"
" > **Note**: this function is not available on the JavaScript target.\n"
).
-spec subject(lustre:runtime(VHT)) -> gleam@erlang@process:subject(lustre@runtime@server@runtime:message(VHT)).
subject(Runtime) ->
gleam@function:identity(Runtime).
-file("src/lustre/server_component.gleam", 243).
?DOC(
" Recover the `Pid` of the server component runtime so that it can be used in\n"
" supervision trees or passed to other processes. If you want to hand out\n"
" different `Subject`s to send messages to your application, take a look at the\n"
" [`select`](#select) effect.\n"
"\n"
" > **Note**: this function is not available on the JavaScript target.\n"
).
-spec pid(lustre:runtime(any())) -> gleam@erlang@process:pid_().
pid(Runtime) ->
_pipe = Runtime,
_pipe@1 = subject(_pipe),
gleam@erlang@process:subject_owner(_pipe@1).
-file("src/lustre/server_component.gleam", 258).
?DOC(
" Register a `Subject` to receive messages and updates from Lustre's server\n"
" component runtime. The process that owns this will be monitored and the\n"
" subject will be gracefully removed if the process dies.\n"
"\n"
" > **Note**: if you are developing a server component for the JavaScript runtime,\n"
" > you should use [`register_callback`](#register_callback) instead.\n"
).
-spec register_subject(
gleam@erlang@process:subject(lustre@runtime@transport:client_message(VIB))
) -> lustre@runtime@server@runtime:message(VIB).
register_subject(Client) ->
{client_registered_subject, Client}.
-file("src/lustre/server_component.gleam", 268).
?DOC(
" Deregister a `Subject` to stop receiving messages and updates from Lustre's\n"
" server component runtime. The subject should first have been registered with\n"
" [`register_subject`](#register_subject) otherwise this will do nothing.\n"
).
-spec deregister_subject(
gleam@erlang@process:subject(lustre@runtime@transport:client_message(VIF))
) -> lustre@runtime@server@runtime:message(VIF).
deregister_subject(Client) ->
{client_deregistered_subject, Client}.
-file("src/lustre/server_component.gleam", 282).
?DOC(
" Register a callback to be called whenever the server component runtime\n"
" produces a message. Avoid using anonymous functions with this function, as\n"
" they cannot later be removed using [`deregister_callback`](#deregister_callback).\n"
"\n"
" > **Note**: server components running on the Erlang target are **strongly**\n"
" > encouraged to use [`register_subject`](#register_subject) instead of this\n"
" > function.\n"
).
-spec register_callback(
fun((lustre@runtime@transport:client_message(VIJ)) -> nil)
) -> lustre@runtime@server@runtime:message(VIJ).
register_callback(Callback) ->
{client_registered_callback, Callback}.
-file("src/lustre/server_component.gleam", 296).
?DOC(
" Deregister a callback to be called whenever the server component runtime\n"
" produces a message. The callback to remove is determined by function equality\n"
" and must be the same function that was passed to [`register_callback`](#register_callback).\n"
"\n"
" > **Note**: server components running on the Erlang target are **strongly**\n"
" > encouraged to use [`register_subject`](#register_subject) instead of this\n"
" > function.\n"
).
-spec deregister_callback(
fun((lustre@runtime@transport:client_message(VIM)) -> nil)
) -> lustre@runtime@server@runtime:message(VIM).
deregister_callback(Callback) ->
{client_deregistered_callback, Callback}.
-file("src/lustre/server_component.gleam", 312).
?DOC(
" Instruct any connected clients to emit a DOM event with the given name and\n"
" data. This lets your server component communicate to the frontend the same way\n"
" any other HTML elements do: you might emit a `\"change\"` event when some part\n"
" of the server component's state changes, for example.\n"
"\n"
" This is a real DOM event and any JavaScript on the page can attach an event\n"
" listener to the server component element and listen for these events.\n"
).
-spec emit(binary(), gleam@json:json()) -> lustre@effect:effect(any()).
emit(Event, Data) ->
lustre@effect:event(Event, Data).
-file("src/lustre/server_component.gleam", 335).
?DOC(
" On the Erlang target, Lustre's server component runtime is an OTP\n"
" [actor](https://hexdocs.pm/gleam_otp/gleam/otp/actor.html) that can be\n"
" communicated with using the standard process API and the `Subject` returned\n"
" when starting the server component.\n"
"\n"
" Sometimes, you might want to hand a different `Subject` to a process to restrict\n"
" the type of messages it can send or to distinguish messages from different\n"
" sources from one another. The `select` effect creates a fresh `Subject` each\n"
" time it is run. By returning a `Selector` you can teach the Lustre server\n"
" component runtime how to listen to messages from this `Subject`.\n"
"\n"
" The `select` effect also gives you the dispatch function passed to `effect.from`.\n"
" This is useful in case you want to store the provided `Subject` in your model\n"
" for later use. For example you may subscribe to a pubsub service and later use\n"
" that same `Subject` to unsubscribe.\n"
"\n"
" > **Note**: This effect does nothing on the JavaScript runtime, where `Subject`s\n"
" > and `Selector`s don't exist, and is the equivalent of returning `effect.none()`.\n"
).
-spec select(
fun((fun((VIR) -> nil), gleam@erlang@process:subject(any())) -> gleam@erlang@process:selector(VIR))
) -> lustre@effect:effect(VIR).
select(Sel) ->
lustre@effect:select(Sel).
-file("src/lustre/server_component.gleam", 348).
?DOC(
" The server component client runtime sends JSON-encoded messages for the server\n"
" runtime to execute. Because your own WebSocket server sits between the two\n"
" parts of the runtime, you need to decode these actions and pass them to the\n"
" server runtime yourself.\n"
).
-spec runtime_message_decoder() -> gleam@dynamic@decode:decoder(lustre@runtime@server@runtime:message(any())).
runtime_message_decoder() ->
gleam@dynamic@decode:map(
lustre@runtime@transport:server_message_decoder(),
fun(Field@0) -> {client_dispatched_message, Field@0} end
).
-file("src/lustre/server_component.gleam", 364).
?DOC(
" Encode a message you can send to the client runtime to respond to. The server\n"
" component runtime will send messages to any registered clients to instruct\n"
" them to update their DOM or emit events, for example.\n"
"\n"
" Because your WebSocket server sits between the two parts of the runtime, you\n"
" need to encode these actions and send them to the client runtime yourself.\n"
).
-spec client_message_to_json(lustre@runtime@transport:client_message(any())) -> gleam@json:json().
client_message_to_json(Message) ->
lustre@runtime@transport:client_message_to_json(Message).