Packages
lustre
5.7.1
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
priv/static/lustre-server-component.mjs
// build/dev/javascript/houdini/houdini.ffi.mjs
function do_escape(string5) {
return string5.replaceAll(/[><&"']/g, (replaced) => {
switch (replaced) {
case ">":
return ">";
case "<":
return "<";
case "'":
return "'";
case "&":
return "&";
case '"':
return """;
default:
return replaced;
}
});
}
// build/dev/javascript/houdini/houdini/internal/escape_js.mjs
function escape(text3) {
return do_escape(text3);
}
// build/dev/javascript/houdini/houdini.mjs
function escape2(string5) {
return escape(string5);
}
// build/dev/javascript/prelude.mjs
var List$NonEmpty$first = (value) => value.head;
var List$NonEmpty$rest = (value) => value.tail;
// build/dev/javascript/gleam_stdlib/dict.mjs
var bits = 5;
var mask = (1 << bits) - 1;
var noElementMarker = Symbol();
var generationKey = Symbol();
// build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs
var unicode_whitespaces = [
" ",
// Space
" ",
// Horizontal tab
"\n",
// Line feed
"\v",
// Vertical tab
"\f",
// Form feed
"\r",
// Carriage return
"\x85",
// Next line
"\u2028",
// Line separator
"\u2029"
// Paragraph separator
].join("");
var trim_start_regex = /* @__PURE__ */ new RegExp(
`^[${unicode_whitespaces}]*`
);
var trim_end_regex = /* @__PURE__ */ new RegExp(`[${unicode_whitespaces}]*$`);
// build/dev/javascript/lustre/lustre/vdom/vattr.mjs
var attribute_kind = 0;
var property_kind = 1;
var event_kind = 2;
var never_kind = 0;
var always_kind = 2;
// build/dev/javascript/lustre/lustre/vdom/vnode.mjs
var fragment_kind = 0;
var element_kind = 1;
var text_kind = 2;
var unsafe_inner_html_kind = 3;
var map_kind = 4;
var memo_kind = 5;
// build/dev/javascript/lustre/lustre/vdom/patch.mjs
var replace_text_kind = 0;
var replace_inner_html_kind = 1;
var update_kind = 2;
var move_kind = 3;
var remove_kind = 4;
var replace_kind = 5;
var insert_kind = 6;
// build/dev/javascript/lustre/lustre/vdom/path.mjs
var separator_subtree = "\r";
var separator_element = " ";
// build/dev/javascript/lustre/lustre/internals/list.ffi.mjs
var iterate = (list4, callback) => {
if (Array.isArray(list4)) {
for (let i = 0; i < list4.length; i++) {
callback(list4[i]);
}
} else if (list4) {
for (list4; List$NonEmpty$rest(list4); list4 = List$NonEmpty$rest(list4)) {
callback(List$NonEmpty$first(list4));
}
}
};
// build/dev/javascript/lustre/lustre/internals/constants.ffi.mjs
var NAMESPACE_HTML = "http://www.w3.org/1999/xhtml";
var SUPPORTS_MOVE_BEFORE = !!globalThis.HTMLElement?.prototype?.moveBefore;
// build/dev/javascript/lustre/lustre/vdom/reconciler.ffi.mjs
var setTimeout2 = globalThis.setTimeout;
var clearTimeout = globalThis.clearTimeout;
var createElementNS = (ns, name) => globalThis.document.createElementNS(ns, name);
var createTextNode = (data) => globalThis.document.createTextNode(data);
var createComment = (data) => globalThis.document.createComment(data);
var createDocumentFragment = () => globalThis.document.createDocumentFragment();
var insertBefore = (parent, node, reference) => parent.insertBefore(node, reference);
var moveBefore = SUPPORTS_MOVE_BEFORE ? (parent, node, reference) => parent.moveBefore(node, reference) : insertBefore;
var removeChild = (parent, child2) => parent.removeChild(child2);
var getAttribute = (node, name) => node.getAttribute(name);
var setAttribute = (node, name, value) => node.setAttribute(name, value);
var removeAttribute = (node, name) => node.removeAttribute(name);
var addEventListener = (node, name, handler, options) => node.addEventListener(name, handler, options);
var removeEventListener = (node, name, handler) => node.removeEventListener(name, handler);
var setInnerHtml = (node, innerHtml) => node.innerHTML = innerHtml;
var setData = (node, data) => node.data = data;
var meta = Symbol("lustre");
var MetadataNode = class {
constructor(kind, parent, node, key) {
this.kind = kind;
this.key = key;
this.parent = parent;
this.children = [];
this.node = node;
this.endNode = null;
this.handlers = /* @__PURE__ */ new Map();
this.throttles = /* @__PURE__ */ new Map();
this.debouncers = /* @__PURE__ */ new Map();
}
get isVirtual() {
return this.kind === fragment_kind || this.kind === map_kind;
}
get parentNode() {
return this.isVirtual ? this.node.parentNode : this.node;
}
};
var insertMetadataChild = (kind, parent, node, index2, key) => {
const child2 = new MetadataNode(kind, parent, node, key);
node[meta] = child2;
parent?.children.splice(index2, 0, child2);
return child2;
};
var getPath = (node) => {
let path = "";
for (let current = node[meta]; current.parent; current = current.parent) {
const separator = current.parent && current.parent.kind === map_kind ? separator_subtree : separator_element;
if (current.key) {
path = `${separator}${current.key}${path}`;
} else {
const index2 = current.parent.children.indexOf(current);
path = `${separator}${index2}${path}`;
}
}
return path.slice(1);
};
var Reconciler = class {
#root = null;
#decodeEvent;
#dispatch;
#debug = false;
constructor(root2, decodeEvent, dispatch2, { debug = false } = {}) {
this.#root = root2;
this.#decodeEvent = decodeEvent;
this.#dispatch = dispatch2;
this.#debug = debug;
}
mount(vdom) {
insertMetadataChild(element_kind, null, this.#root, 0, null);
this.#insertChild(this.#root, null, this.#root[meta], 0, vdom);
}
push(patch, memos2 = null) {
this.#memos = memos2;
this.#stack.push({ node: this.#root[meta], patch });
this.#reconcile();
}
// PATCHING ------------------------------------------------------------------
#memos;
#stack = [];
#reconcile() {
const stack = this.#stack;
while (stack.length) {
let { node, patch } = stack.pop();
const { path, changes, removed, children: childPatches } = patch;
iterate(path, (index2) => {
node = node.children[index2];
});
const { children: childNodes } = node;
iterate(changes, (change) => this.#patch(node, change));
if (removed) {
this.#removeChildren(node, childNodes.length - removed, removed);
}
iterate(childPatches, (childPatch) => {
const child2 = childNodes[childPatch.index | 0];
this.#stack.push({ node: child2, patch: childPatch });
});
}
}
#patch(node, change) {
switch (change.kind) {
case replace_text_kind:
this.#replaceText(node, change);
break;
case replace_inner_html_kind:
this.#replaceInnerHtml(node, change);
break;
case update_kind:
this.#update(node, change);
break;
case move_kind:
this.#move(node, change);
break;
case remove_kind:
this.#remove(node, change);
break;
case replace_kind:
this.#replace(node, change);
break;
case insert_kind:
this.#insert(node, change);
break;
}
}
// CHANGES -------------------------------------------------------------------
#insert(parent, { children, before }) {
const fragment3 = createDocumentFragment();
const beforeEl = this.#getReference(parent, before);
this.#insertChildren(fragment3, null, parent, before | 0, children);
insertBefore(parent.parentNode, fragment3, beforeEl);
}
#replace(parent, { index: index2, with: child2 }) {
this.#removeChildren(parent, index2 | 0, 1);
const beforeEl = this.#getReference(parent, index2);
this.#insertChild(parent.parentNode, beforeEl, parent, index2 | 0, child2);
}
#getReference(node, index2) {
index2 = index2 | 0;
const { children } = node;
const childCount = children.length;
if (index2 < childCount) return children[index2].node;
if (node.endNode) return node.endNode;
if (!node.isVirtual) return null;
while (node.isVirtual && node.children.length) {
if (node.endNode) return node.endNode.nextSibling;
node = node.children[node.children.length - 1];
}
return node.node.nextSibling;
}
#move(parent, { key, before }) {
before = before | 0;
const { children, parentNode } = parent;
const beforeEl = children[before].node;
let prev = children[before];
for (let i = before + 1; i < children.length; ++i) {
const next = children[i];
children[i] = prev;
prev = next;
if (next.key === key) {
children[before] = next;
break;
}
}
this.#moveChild(parentNode, prev, beforeEl);
}
#moveChildren(domParent, children, beforeEl) {
for (let i = 0; i < children.length; ++i) {
this.#moveChild(domParent, children[i], beforeEl);
}
}
#moveChild(domParent, child2, beforeEl) {
moveBefore(domParent, child2.node, beforeEl);
if (child2.isVirtual) {
this.#moveChildren(domParent, child2.children, beforeEl);
}
if (child2.endNode) {
moveBefore(domParent, child2.endNode, beforeEl);
}
}
#remove(parent, { index: index2 }) {
this.#removeChildren(parent, index2, 1);
}
#removeChildren(parent, index2, count) {
const { children, parentNode } = parent;
const deleted = children.splice(index2, count);
for (let i = 0; i < deleted.length; ++i) {
const child2 = deleted[i];
const { node, endNode, isVirtual, children: nestedChildren } = child2;
removeChild(parentNode, node);
if (endNode) {
removeChild(parentNode, endNode);
}
this.#removeDebouncers(child2);
if (isVirtual) {
deleted.push(...nestedChildren);
}
}
}
#removeDebouncers(node) {
const { debouncers, children } = node;
for (const { timeout } of debouncers.values()) {
if (timeout) {
clearTimeout(timeout);
}
}
debouncers.clear();
iterate(children, (child2) => this.#removeDebouncers(child2));
}
#update({ node, handlers, throttles, debouncers }, { added, removed }) {
iterate(removed, ({ name }) => {
if (handlers.delete(name)) {
removeEventListener(node, name, handleEvent);
this.#updateDebounceThrottle(throttles, name, 0);
this.#updateDebounceThrottle(debouncers, name, 0);
} else {
removeAttribute(node, name);
SYNCED_ATTRIBUTES[name]?.removed?.(node, name);
}
});
iterate(added, (attribute3) => this.#createAttribute(node, attribute3));
}
#replaceText({ node }, { content }) {
setData(node, content ?? "");
}
#replaceInnerHtml({ node }, { inner_html }) {
setInnerHtml(node, inner_html ?? "");
}
// INSERT --------------------------------------------------------------------
#insertChildren(domParent, beforeEl, metaParent, index2, children) {
iterate(
children,
(child2) => this.#insertChild(domParent, beforeEl, metaParent, index2++, child2)
);
}
#insertChild(domParent, beforeEl, metaParent, index2, vnode) {
switch (vnode.kind) {
case element_kind: {
const node = this.#createElement(metaParent, index2, vnode);
this.#insertChildren(node, null, node[meta], 0, vnode.children);
insertBefore(domParent, node, beforeEl);
break;
}
case text_kind: {
const node = this.#createTextNode(metaParent, index2, vnode);
insertBefore(domParent, node, beforeEl);
break;
}
case fragment_kind: {
const marker = "lustre:fragment";
const head = this.#createHead(marker, metaParent, index2, vnode);
insertBefore(domParent, head, beforeEl);
this.#insertChildren(
domParent,
beforeEl,
head[meta],
0,
vnode.children
);
if (this.#debug) {
head[meta].endNode = createComment(` /${marker} `);
insertBefore(domParent, head[meta].endNode, beforeEl);
}
break;
}
case unsafe_inner_html_kind: {
const node = this.#createElement(metaParent, index2, vnode);
this.#replaceInnerHtml({ node }, vnode);
insertBefore(domParent, node, beforeEl);
break;
}
case map_kind: {
const head = this.#createHead("lustre:map", metaParent, index2, vnode);
insertBefore(domParent, head, beforeEl);
this.#insertChild(domParent, beforeEl, head[meta], 0, vnode.child);
break;
}
case memo_kind: {
const child2 = this.#memos?.get(vnode.view) ?? vnode.view();
this.#insertChild(domParent, beforeEl, metaParent, index2, child2);
break;
}
}
}
#createElement(parent, index2, { kind, key, tag, namespace, attributes }) {
const node = createElementNS(namespace || NAMESPACE_HTML, tag);
insertMetadataChild(kind, parent, node, index2, key);
if (this.#debug && key) {
setAttribute(node, "data-lustre-key", key);
}
iterate(attributes, (attribute3) => this.#createAttribute(node, attribute3));
return node;
}
#createTextNode(parent, index2, { kind, key, content }) {
const node = createTextNode(content ?? "");
insertMetadataChild(kind, parent, node, index2, key);
return node;
}
#createHead(marker, parent, index2, { kind, key }) {
const node = this.#debug ? createComment(markerComment(marker, key)) : createTextNode("");
insertMetadataChild(kind, parent, node, index2, key);
return node;
}
#createAttribute(node, attribute3) {
const { debouncers, handlers, throttles } = node[meta];
const {
kind,
name,
value,
prevent_default: prevent,
debounce: debounceDelay,
throttle: throttleDelay
} = attribute3;
switch (kind) {
case attribute_kind: {
const valueOrDefault = value ?? "";
if (name === "virtual:defaultValue") {
node.defaultValue = valueOrDefault;
return;
} else if (name === "virtual:defaultChecked") {
node.defaultChecked = true;
return;
} else if (name === "virtual:defaultSelected") {
node.defaultSelected = true;
return;
}
if (valueOrDefault !== getAttribute(node, name)) {
setAttribute(node, name, valueOrDefault);
}
SYNCED_ATTRIBUTES[name]?.added?.(node, valueOrDefault);
break;
}
case property_kind:
node[name] = value;
break;
case event_kind: {
if (handlers.has(name)) {
removeEventListener(node, name, handleEvent);
}
const passive = prevent.kind === never_kind;
addEventListener(node, name, handleEvent, { passive });
this.#updateDebounceThrottle(throttles, name, throttleDelay);
this.#updateDebounceThrottle(debouncers, name, debounceDelay);
handlers.set(name, (event2) => this.#handleEvent(attribute3, event2));
break;
}
}
}
#updateDebounceThrottle(map6, name, delay) {
const debounceOrThrottle = map6.get(name);
if (delay > 0) {
if (debounceOrThrottle) {
debounceOrThrottle.delay = delay;
} else {
map6.set(name, { delay });
}
} else if (debounceOrThrottle) {
const { timeout } = debounceOrThrottle;
if (timeout) {
clearTimeout(timeout);
}
map6.delete(name);
}
}
#handleEvent(attribute3, event2) {
const { currentTarget, type } = event2;
const { debouncers, throttles } = currentTarget[meta];
const path = getPath(currentTarget);
const {
prevent_default: prevent,
stop_propagation: stop,
include
} = attribute3;
if (prevent.kind === always_kind) event2.preventDefault();
if (stop.kind === always_kind) event2.stopPropagation();
if (type === "submit") {
event2.detail ??= {};
event2.detail.formData = [
...new FormData(event2.target, event2.submitter).entries()
];
}
const data = this.#decodeEvent(event2, path, type, include);
const throttle = throttles.get(type);
if (throttle) {
const now = Date.now();
const last = throttle.last || 0;
if (now > last + throttle.delay) {
throttle.last = now;
throttle.lastEvent = event2;
this.#dispatch(event2, data);
}
}
const debounce = debouncers.get(type);
if (debounce) {
clearTimeout(debounce.timeout);
debounce.timeout = setTimeout2(() => {
if (event2 === throttles.get(type)?.lastEvent) return;
this.#dispatch(event2, data);
}, debounce.delay);
}
if (!throttle && !debounce) {
this.#dispatch(event2, data);
}
}
};
var markerComment = (marker, key) => {
if (key) {
return ` ${marker} key="${escape2(key)}" `;
} else {
return ` ${marker} `;
}
};
var handleEvent = (event2) => {
const { currentTarget, type } = event2;
const handler = currentTarget[meta].handlers.get(type);
handler(event2);
};
var syncedBooleanAttribute = /* @__NO_SIDE_EFFECTS__ */ (name) => {
return {
added(node) {
node[name] = true;
},
removed(node) {
node[name] = false;
}
};
};
var syncedAttribute = /* @__NO_SIDE_EFFECTS__ */ (name) => {
return {
added(node, value) {
node[name] = value;
}
};
};
var SYNCED_ATTRIBUTES = {
checked: /* @__PURE__ */ syncedBooleanAttribute("checked"),
selected: /* @__PURE__ */ syncedBooleanAttribute("selected"),
value: /* @__PURE__ */ syncedAttribute("value"),
autofocus: {
added(node) {
queueMicrotask(() => {
node.focus?.();
});
}
},
autoplay: {
added(node) {
try {
node.play?.();
} catch (e) {
console.error(e);
}
}
}
};
// build/dev/javascript/lustre/lustre/runtime/client/runtime.ffi.mjs
var copiedStyleSheets = /* @__PURE__ */ new WeakMap();
async function adoptStylesheets(shadowRoot) {
const pendingParentStylesheets = [];
for (const node of globalThis.document.querySelectorAll(
"link[rel=stylesheet], style"
)) {
if (node.sheet) continue;
pendingParentStylesheets.push(
new Promise((resolve, reject) => {
node.addEventListener("load", resolve);
node.addEventListener("error", reject);
})
);
}
await Promise.allSettled(pendingParentStylesheets);
if (!shadowRoot.host.isConnected) {
return [];
}
shadowRoot.adoptedStyleSheets = shadowRoot.host.getRootNode().adoptedStyleSheets;
const pending = [];
for (const sheet of globalThis.document.styleSheets) {
try {
shadowRoot.adoptedStyleSheets.push(sheet);
} catch {
try {
let copiedSheet = copiedStyleSheets.get(sheet);
if (!copiedSheet) {
copiedSheet = new CSSStyleSheet();
for (const rule of sheet.cssRules) {
copiedSheet.insertRule(rule.cssText, copiedSheet.cssRules.length);
}
copiedStyleSheets.set(sheet, copiedSheet);
}
shadowRoot.adoptedStyleSheets.push(copiedSheet);
} catch {
const node = sheet.ownerNode.cloneNode();
shadowRoot.prepend(node);
pending.push(node);
}
}
}
return pending;
}
var ContextRequestEvent = class extends Event {
constructor(context, callback, subscribe) {
super("context-request", { bubbles: true, composed: true });
this.context = context;
this.callback = callback;
this.subscribe = subscribe;
}
};
var LustreEvent = class extends CustomEvent {
// We can't rely on `instanceof` checks because the server component client
// runtime is bundled on its own and thus will have its own copy of this class.
isLustreEvent = true;
constructor(name, detail) {
super(name, { detail, bubbles: true, composed: true });
}
};
// build/dev/javascript/lustre/lustre/runtime/transport.mjs
var mount_kind = 0;
var reconcile_kind = 1;
var emit_kind = 2;
var provide_kind = 3;
var subscribe_kind = 4;
var unsubscribe_kind = 5;
var attribute_changed_kind = 0;
var event_fired_kind = 1;
var property_changed_kind = 2;
var batch_kind = 3;
var context_provided_kind = 4;
// src/lustre/runtime/client/server_component.ffi.mjs
var ServerComponent = class extends HTMLElement {
static get observedAttributes() {
return ["route", "method", "csrf-token"];
}
#shadowRoot;
#method = "ws";
#route = null;
#csrfToken = null;
#transport = null;
#adoptedStyleNodes = [];
#reconciler;
#remoteObservedAttributes = /* @__PURE__ */ new Set();
#remoteObservedProperties = /* @__PURE__ */ new Set();
#connected = false;
#changedAttributesQueue = [];
#contexts = /* @__PURE__ */ new Map();
#contextSubscriptions = /* @__PURE__ */ new Map();
#observer = new MutationObserver((mutations) => {
const attributes = [];
for (const mutation of mutations) {
if (mutation.type !== "attributes") continue;
const name = mutation.attributeName;
if (!this.#connected || this.#remoteObservedAttributes.has(name)) {
attributes.push([name, this.getAttribute(name)]);
}
}
if (attributes.length === 1) {
const [name, value] = attributes[0];
this.#transport?.send({ kind: attribute_changed_kind, name, value });
} else if (attributes.length) {
this.#transport?.send({
kind: batch_kind,
messages: attributes.map(([name, value]) => ({
kind: attribute_changed_kind,
name,
value
}))
});
} else {
this.#changedAttributesQueue.push(...attributes);
}
});
constructor() {
super();
this.internals = this.attachInternals();
this.#observer.observe(this, {
attributes: true
});
}
connectedCallback() {
for (const attribute3 of this.attributes) {
this.#changedAttributesQueue.push([attribute3.name, attribute3.value]);
}
}
attributeChangedCallback(name, prev, next) {
switch (name) {
case (prev !== next && "route"): {
this.#route = new URL(next, location.href);
this.#csrfToken = this.#getCsrfToken();
this.#route.searchParams.set("csrf-token", this.#csrfToken);
this.#connect();
return;
}
case "method": {
const normalised = next.toLowerCase();
if (normalised == this.#method) return;
if (["ws", "sse", "polling"].includes(normalised)) {
this.#method = normalised;
if (this.#method == "ws") {
if (this.#route.protocol == "https:") this.#route.protocol = "wss:";
if (this.#route.protocol == "http:") this.#route.protocol = "ws:";
}
this.#connect();
}
return;
}
case "csrf-token": {
if (prev !== next && this.#connected) {
this.#transport?.close();
}
this.#csrfToken = this.#getCsrfToken();
if (this.#route) {
this.#route.searchParams.set("csrf-token", this.#csrfToken);
}
if (this.#connected) {
this.#connect();
}
}
}
}
async messageReceivedCallback(data) {
switch (data.kind) {
case mount_kind: {
this.#shadowRoot ??= this.attachShadow({
mode: data.open_shadow_root ? "open" : "closed"
});
while (this.#shadowRoot.firstChild) {
this.#shadowRoot.firstChild.remove();
}
const decodeEvent = (event2, path, name, include) => {
const data2 = this.#createServerEvent(event2, include ?? []);
return {
kind: event_fired_kind,
path,
name,
event: data2
};
};
const dispatch2 = (event2, data2) => {
this.#transport?.send(data2);
};
this.#reconciler = new Reconciler(
this.#shadowRoot,
decodeEvent,
dispatch2
);
this.#remoteObservedAttributes = new Set(data.observed_attributes);
const filteredQueuedAttributes = this.#changedAttributesQueue.filter(
([name]) => this.#remoteObservedAttributes.has(name)
);
const messages = filteredQueuedAttributes.map(([name, value]) => ({
kind: attribute_changed_kind,
name,
value
}));
this.#changedAttributesQueue = [];
this.#remoteObservedProperties = new Set(data.observed_properties);
for (const name of this.#remoteObservedProperties) {
Object.defineProperty(this, name, {
get() {
return this[`_${name}`];
},
set(value) {
this[`_${name}`] = value;
this.#transport?.send({
kind: property_changed_kind,
name,
value
});
}
});
}
for (const [key, value] of Object.entries(data.provided_contexts)) {
this.provide(key, value);
}
for (const key of [...new Set(data.requested_contexts)]) {
this.subscribe(key);
}
if (messages.length) {
this.#transport.send({
kind: batch_kind,
messages
});
}
if (data.will_adopt_styles) {
await this.#adoptStyleSheets();
}
this.#shadowRoot.addEventListener("context-request", (event2) => {
if (!event2.context || !event2.callback) return;
if (!this.#contexts.has(event2.context)) return;
event2.stopImmediatePropagation();
const context = this.#contexts.get(event2.context);
if (event2.subscribe) {
const unsubscribe = () => {
context.subscribers = context.subscribers.filter(
(subscriber) => subscriber !== event2.callback
);
};
context.subscribers.push([event2.callback, unsubscribe]);
event2.callback(context.value, unsubscribe);
} else {
event2.callback(context.value);
}
});
this.#reconciler.mount(data.vdom);
this.dispatchEvent(new CustomEvent("lustre:mount"));
break;
}
case reconcile_kind: {
this.#reconciler.push(data.patch);
break;
}
case emit_kind: {
this.dispatchEvent(new LustreEvent(data.name, data.data));
break;
}
case provide_kind: {
this.provide(data.key, data.value);
break;
}
case subscribe_kind: {
this.subscribe(data.key);
break;
}
case unsubscribe_kind: {
this.unsubscribe(data.key);
break;
}
}
}
//
disconnectedCallback() {
this.unsubscribeAll();
if (this.#transport) {
this.#transport.close();
this.#transport = null;
}
}
// Context provider method
provide(key, value) {
if (!this.#contexts.has(key)) {
this.#contexts.set(key, { value, subscribers: [] });
} else {
const context = this.#contexts.get(key);
context.value = value;
for (let i = context.subscribers.length - 1; i >= 0; i--) {
const [subscriber, unsubscribe] = context.subscribers[i];
if (!subscriber) {
context.subscribers.splice(i, 1);
continue;
}
subscriber(value, unsubscribe);
}
}
}
subscribe(key) {
if (!key) return;
this.#contextSubscriptions.get(key)?.();
this.dispatchEvent(
new ContextRequestEvent(key, (value, unsubscribe) => {
this.#transport?.send({
kind: context_provided_kind,
key,
value
});
this.#contextSubscriptions.get(key)?.();
this.#contextSubscriptions.set(unsubscribe);
})
);
}
unsubscribe(key) {
this.#contextSubscriptions.get(key)?.();
this.#contextSubscriptions.delete(key);
}
unsubscribeAll() {
for (const [_, unsubscribe] of this.#contextSubscriptions) {
unsubscribe?.();
}
this.#contextSubscriptions.clear();
}
#getCsrfToken() {
if (this.hasAttribute("csrf-token")) {
return this.getAttribute("csrf-token") || null;
} else {
const meta2 = document.querySelector('meta[name="csrf-token"]');
const token = meta2?.getAttribute("content");
return token || null;
}
}
#connect() {
if (!this.#route || !this.#method) return;
if (this.#transport) this.#transport.close();
const onConnect = () => {
this.#connected = true;
this.dispatchEvent(new CustomEvent("lustre:connect"), {
detail: {
route: this.#route,
method: this.#method
}
});
};
const onMessage = (data) => {
this.messageReceivedCallback(data);
};
const onClose = () => {
this.#connected = false;
this.dispatchEvent(
new CustomEvent("lustre:close", {
detail: {
route: this.#route,
method: this.#method
}
})
);
};
const options = {
onConnect,
onMessage,
onClose,
csrfToken: this.#csrfToken
};
switch (this.#method) {
case "ws":
this.#transport = new WebsocketTransport(this.#route, options);
break;
case "sse":
this.#transport = new SseTransport(this.#route, options);
break;
case "polling":
this.#transport = new PollingTransport(this.#route, options);
break;
}
}
//
async #adoptStyleSheets() {
while (this.#adoptedStyleNodes.length) {
this.#adoptedStyleNodes.pop().remove();
this.#shadowRoot.firstChild.remove();
}
this.#adoptedStyleNodes = await adoptStylesheets(this.#shadowRoot);
}
/** Server components send the event data as a JSON object over the network to
* the server component runtime. Out of the box this would effectively do nothing
* because the event object is not serialisable: almost every property is
* non-enumerable.
*
* To counter this, users can provide a list of properties they'd like the runtime
* to include in the event data. Each property is a dot-separated string that
* represents the traversal path to the desired property.
*
*/
#createServerEvent(event2, include = []) {
const data = {};
if (event2.isLustreEvent) {
include.push("detail");
}
if (event2.type === "input" || event2.type === "change") {
if (event2.target.type === "checkbox") {
include.push("target.checked");
} else {
include.push("target.value");
}
}
if (event2.type === "keydown" || event2.type === "keyup" || event2.type === "keypress") {
include.push("key");
}
if (event2.type === "submit") {
include.push("detail.formData");
}
for (const property2 of include) {
const path = property2.split(".");
for (let i = 0, input = event2, output = data; i < path.length; i++) {
if (i === path.length - 1) {
output[path[i]] = input[path[i]];
break;
}
output = output[path[i]] ??= {};
input = input[path[i]];
}
}
return data;
}
};
var WebsocketTransport = class {
#url;
#socket;
#waitingForResponse = false;
#queue = [];
#shouldReconnect = true;
#reconnectDelay = 500;
#maxReconnectDelay = 1e4;
#onConnect;
#onMessage;
#onClose;
constructor(url, { onConnect, onMessage, onClose }) {
this.#url = url;
this.#onConnect = onConnect;
this.#onMessage = onMessage;
this.#onClose = onClose;
this.#connect();
}
#connect() {
this.#socket = new WebSocket(this.#url);
this.#shouldReconnect = true;
this.#queue = [];
this.#socket.onopen = () => {
this.#reconnectDelay = 500;
this.#onConnect();
};
this.#socket.onmessage = ({ data }) => {
try {
this.#onMessage(JSON.parse(data));
} finally {
if (this.#queue.length) {
this.#socket.send(
JSON.stringify({
kind: batch_kind,
messages: this.#queue
})
);
} else {
this.#waitingForResponse = false;
}
this.#queue = [];
}
};
this.#socket.onclose = (event2) => {
this.#onClose();
if (event2.code !== 1e3 && this.#shouldReconnect) {
this.#attemptReconnect();
}
};
}
#attemptReconnect() {
const reconnect = () => {
if (!this.#shouldReconnect) return;
this.#connect();
this.#reconnectDelay = Math.min(
this.#reconnectDelay * 2,
this.#maxReconnectDelay
);
};
if (document.hidden) {
const handleVisibilityChange = () => {
if (!document.hidden && this.#shouldReconnect) {
document.removeEventListener(
"visibilitychange",
handleVisibilityChange
);
reconnect();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
} else {
setTimeout(reconnect, this.#reconnectDelay);
}
}
send(data) {
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) return;
if (this.#waitingForResponse) {
this.#queue.push(data);
return;
} else {
this.#socket.send(JSON.stringify(data));
this.#waitingForResponse = true;
}
}
close() {
this.#shouldReconnect = false;
this.#socket.close(1e3);
this.#socket = null;
}
};
var SseTransport = class {
#url;
#eventSource;
#shouldReconnect = true;
#reconnectDelay = 500;
#maxReconnectDelay = 1e4;
#onConnect;
#onMessage;
#onClose;
constructor(url, { onConnect, onMessage, onClose }) {
this.#url = url;
this.#onConnect = onConnect;
this.#onMessage = onMessage;
this.#onClose = onClose;
this.#connect();
}
#connect() {
this.#eventSource = new EventSource(this.#url);
this.#reconnectDelay = 500;
this.#shouldReconnect = true;
this.#eventSource.onopen = () => {
this.#onConnect();
};
this.#eventSource.onmessage = ({ data }) => {
try {
this.#onMessage(JSON.parse(data));
} catch {
}
};
this.#eventSource.onerror = () => {
this.#eventSource.close();
this.#onClose();
if (this.#shouldReconnect) {
this.#attemptReconnect();
}
};
}
#attemptReconnect() {
const reconnect = () => {
if (!this.#shouldReconnect) return;
this.#connect();
this.#reconnectDelay = Math.min(
this.#reconnectDelay * 2,
this.#maxReconnectDelay
);
};
if (document.hidden) {
const handleVisibilityChange = () => {
if (!document.hidden && this.#shouldReconnect) {
document.removeEventListener(
"visibilitychange",
handleVisibilityChange
);
reconnect();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
} else {
setTimeout(reconnect, this.#reconnectDelay);
}
}
send(data) {
}
close() {
this.#shouldReconnect = false;
this.#eventSource.close();
this.#onClose();
}
};
var PollingTransport = class {
#url;
#csrfToken;
#interval;
#timer;
#onConnect;
#onMessage;
#onClose;
constructor(url, { onConnect, onMessage, onClose, csrfToken, interval }) {
this.#url = url;
this.#csrfToken = csrfToken;
this.#interval = interval ?? 5e3;
this.#onConnect = onConnect;
this.#onMessage = onMessage;
this.#onClose = onClose;
this.#fetch().finally(() => {
this.#onConnect();
this.#timer = setInterval(() => this.#fetch(), this.#interval);
});
}
async send(data) {
}
close() {
clearInterval(this.#timer);
this.#onClose();
}
#fetch() {
const headers = Object.assign(
{},
// If a CSRF token is provided, include it as a request header as checking
// headers is more common than query param for CSRF protection in traditional
// HTTP requests.
this.#csrfToken && { "x-csrf-token": this.#csrfToken }
);
return fetch(this.#url, { headers }).then((response) => response.json()).then(this.#onMessage).catch(console.error);
}
};
customElements.define("lustre-server-component", ServerComponent);
export {
ServerComponent
};