diff --git a/examples/chat/components/header.js b/examples/chat/components/header.js
new file mode 100644
index 0000000..9817f43
--- /dev/null
+++ b/examples/chat/components/header.js
@@ -0,0 +1,31 @@
+import {watch} from "../lib/signaller.js";
+import {local_peer_id} from "../state.js";
+
+export default class ChatHeader extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ const shadow = this.attachShadow({mode: "open"});
+ shadow.innerHTML = `
+
+
+ `;
+ const setText = () => {
+ const span = shadow.getElementById('local-peer-id');
+ span.textContent = `You: ${local_peer_id.value}`;
+ };
+ setText();
+ this.stop = watch([local_peer_id], setText);
+ }
+
+ disconnectedCallback() {
+ this.stop();
+ }
+}
+
+customElements.define("chat-header", ChatHeader);
diff --git a/examples/chat/components/message-history.js b/examples/chat/components/message-history.js
new file mode 100644
index 0000000..f4ac7f4
--- /dev/null
+++ b/examples/chat/components/message-history.js
@@ -0,0 +1,48 @@
+import {watch} from "../lib/signaller.js";
+import {local_peer_id, selected_invite, messages} from "../state.js";
+
+export default class ChatMessageHistory extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.shadow = this.attachShadow({mode: "open"});
+ this.shadow.innerHTML = `
+
+
+ `;
+ this.history = this.shadow.getElementById('message-history');
+ this.stop = watch([local_peer_id, selected_invite, messages], () => {
+ this.updateMessages();
+ });
+ this.updateMessages();
+ }
+
+ disconnectedCallback() {
+ this.stop();
+ }
+
+ updateMessages() {
+ let txt = "";
+ if (selected_invite.value) {
+ for (const msg of messages.value) {
+ const match_from = (
+ msg.from.peer === selected_invite.value.peer &&
+ msg.from.app_instance_uuid === selected_invite.value.app_instance_uuid
+ );
+ const match_to = (
+ msg.to.peer === selected_invite.value.peer &&
+ msg.to.app_instance_uuid === selected_invite.value.app_instance_uuid
+ );
+ if (match_from || match_to) {
+ const from = msg.from.peer === local_peer_id.value ? 'You' : msg.from.peer;
+ txt += `<${from}>: ${msg.message}\n`;
+ }
+ }
+ }
+ this.history.textContent = txt;
+ }
+}
+
+customElements.define("chat-message-history", ChatMessageHistory);
diff --git a/examples/chat/components/peers.js b/examples/chat/components/peers.js
new file mode 100644
index 0000000..ff54d74
--- /dev/null
+++ b/examples/chat/components/peers.js
@@ -0,0 +1,62 @@
+import {watch} from "../lib/signaller.js";
+import {delegate} from "../lib/events.js";
+import {invites, selected_invite} from "../state.js";
+
+export default class ChatPeers extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.shadow = this.attachShadow({mode: "open"});
+ this.shadow.innerHTML = `
+
+
+ `;
+ this.peers = this.shadow.getElementById('peers');
+ this.cleanup = [
+ watch([invites], () => this.updatePeers()),
+ watch([selected_invite], () => this.updateSelected()),
+ delegate(this.peers, "click", "#peers li", function () {
+ selected_invite.value = JSON.parse(this.dataset.invite);
+ }),
+ ];
+ this.updatePeers();
+ }
+
+ disconnectedCallback() {
+ for (const destroy of this.cleanup) destroy();
+ }
+
+ updatePeers() {
+ this.peers.innerHTML = '';
+ if (invites.value.length === 0) {
+ const span = document.createElement('span');
+ span.textContent = "No peers discovered yet";
+ this.peers.appendChild(span);
+ } else {
+ const ul = document.createElement('ul');
+ for (const invite of invites.value) {
+ const li = document.createElement('li');
+ li.textContent = invite.peer;
+ li.dataset.invite = JSON.stringify(invite);
+ ul.appendChild(li);
+ }
+ this.peers.appendChild(ul);
+ }
+ this.updateSelected();
+ }
+
+ updateSelected() {
+ const json = JSON.stringify(selected_invite.value);
+ for (const li of this.peers.querySelectorAll("li")) {
+ if (li.dataset.invite === json) {
+ li.classList.add("active");
+ } else {
+ li.classList.remove("active");
+ }
+ }
+ }
+}
+
+customElements.define("chat-peers", ChatPeers);
diff --git a/examples/chat/components/send-message-form.js b/examples/chat/components/send-message-form.js
new file mode 100644
index 0000000..eeaf200
--- /dev/null
+++ b/examples/chat/components/send-message-form.js
@@ -0,0 +1,66 @@
+import {bind} from "../lib/events.js";
+import {watch} from "../lib/signaller.js";
+import {appendMessage, local_peer_id, local_app_uuid, selected_invite} from "../state.js";
+
+export default class ChatSendMessageForm extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.shadow = this.attachShadow({mode: "open"});
+ this.shadow.innerHTML = `
+
+
+ `;
+ this.form = this.shadow.getElementById('send-message-form');
+ this.input = this.shadow.querySelector('[name=message]');
+ this.cleanup = [
+ bind(this.form, 'submit', ev => this.submit(ev)),
+ watch([selected_invite], () => this.updateVisibility()),
+ ];
+ }
+
+ disconnectedCallback() {
+ for (const destroy of this.cleanup) destroy();
+ }
+
+ updateVisibility() {
+ if (selected_invite.value) {
+ this.form.style.display = 'flex';
+ this.input.focus();
+ } else {
+ this.form.style.display = 'none';
+ }
+ }
+
+ async submit(ev) {
+ ev.preventDefault();
+ if (selected_invite.value && local_peer_id.value && local_app_uuid.value) {
+ const message = this.input.value;
+ await fetch("/_api/v1/message_send", {
+ method: "POST",
+ body: JSON.stringify({
+ peer: selected_invite.value.peer,
+ app_instance_uuid: selected_invite.value.app_instance_uuid,
+ message,
+ })
+ });
+ this.input.value = "";
+ const from = {
+ peer: local_peer_id.value,
+ app_instance_uuid: local_app_uuid.value,
+ };
+ const to = {
+ peer: selected_invite.value.peer,
+ app_instance_uuid: selected_invite.value.app_instance_uuid,
+ };
+ appendMessage(from, to, message);
+ }
+ }
+}
+
+customElements.define("chat-send-message-form", ChatSendMessageForm);
diff --git a/examples/chat/index.html b/examples/chat/index.html
index 838289e..aed79f7 100644
--- a/examples/chat/index.html
+++ b/examples/chat/index.html
@@ -8,18 +8,12 @@
-
+
-
+
diff --git a/examples/chat/events.js b/examples/chat/lib/events.js
similarity index 70%
rename from examples/chat/events.js
rename to examples/chat/lib/events.js
index 2c5fb10..929e269 100644
--- a/examples/chat/events.js
+++ b/examples/chat/lib/events.js
@@ -13,3 +13,9 @@ export function delegate(node, event_name, selector, listener, options) {
// Return undelegate function
return () => node.removeEventListener(event_name, fn);
}
+
+export function bind(node, event_name, listener, options) {
+ node.addEventListener(event_name, listener, options);
+ // Return unbind function
+ return () => node.removeEventListener(event_name, listener);
+}
diff --git a/examples/chat/signaller.js b/examples/chat/lib/signaller.js
similarity index 100%
rename from examples/chat/signaller.js
rename to examples/chat/lib/signaller.js
diff --git a/examples/chat/main.js b/examples/chat/main.js
index eda265d..1598aeb 100644
--- a/examples/chat/main.js
+++ b/examples/chat/main.js
@@ -1,21 +1,14 @@
// @ts-check
-import {Signaller, watch} from "./signaller.js";
-import {delegate} from "./events.js";
+import * as state from "./state.js";
-/** @typedef {{peer: string, uuid: string}} Invite */
-/** @typedef {{peer: string, app_instance_uuid: string}} AppInstance */
-/** @typedef {{message: string, from: AppInstance, to: AppInstance}} Message */
-
-// State
-const local_peer_id = new Signaller(/** @type {null | string} */(null));
-const local_app_uuid = new Signaller(/** @type {null | string} */(null));
-const selected_invite = new Signaller(/** @type {null | AppInstance} */(null));
-const peers = new Signaller(new Set());
-const messages = new Signaller(/** @type {Message[]} */([]));
-const invites = new Signaller(/** @type {Invite[]} */([]));
+// Custom elements
+import "./components/header.js";
+import "./components/peers.js";
+import "./components/message-history.js";
+import "./components/send-message-form.js";
async function updateLocalPeerId() {
- local_peer_id.value = await fetch("/_api/v1/local_peer_id").then(
+ state.local_peer_id.value = await fetch("/_api/v1/local_peer_id").then(
res => res.text()
);
}
@@ -24,67 +17,48 @@ async function updateLocalAppInstance() {
const data = await fetch("/_api/v1/application_instance").then(
res => res.json()
);
- local_app_uuid.value = data.uuid;
-}
-
-/** @param {{peer: string, uuid: string}} invite */
-function renderInvite(invite) {
- const li = document.createElement('li');
- li.textContent = invite.peer;
- li.dataset.invite = JSON.stringify(invite);
- return li;
+ state.local_app_uuid.value = data.uuid;
}
-/** @param {{peer: string, uuid: string}[]} invites */
async function updatePeers() {
const res = await fetch("/_api/v1/peers");
const data = new Set(await res.json());
// Send invites to newly discovered peers
- const new_peers = data.difference(peers.value);
+ const new_peers = data.difference(state.peers.value);
for (const peer of new_peers) {
await fetch("/_api/v1/message_invite", {
method: 'POST',
body: JSON.stringify({peer}),
});
}
- peers.value = data;
+ state.peers.value = data;
}
async function updateInvites() {
const res = await fetch("/_api/v1/message_invites");
const data = /** @type {{peer: string, uuid: string}[]} */(await res.json());
// Only list invites for peers in current discovered list
- const new_invites = data.filter(x => peers.value.has(x.peer));
+ const new_invites = data.filter(x => state.peers.value.has(x.peer));
// Update state only if invites have changed
- if (JSON.stringify(new_invites) !== JSON.stringify(invites.value)) {
- invites.value = new_invites;
+ if (JSON.stringify(new_invites) !== JSON.stringify(state.invites.value)) {
+ state.invites.value = new_invites;
}
}
-/**
- * @param {AppInstance} from
- * @param {AppInstance} to
- * @param {string} message
- */
-function appendMessage(from, to, message) {
- messages.value.push({from, to, message});
- messages.signal();
-}
-
async function getMessages() {
while (true) {
const res = await fetch("/_api/v1/message_read");
const data = await res.json();
- if (data && local_peer_id.value && local_app_uuid.value) {
+ if (data && state.local_peer_id.value && state.local_app_uuid.value) {
const from = {
peer: data.peer,
app_instance_uuid: data.uuid,
};
const to = {
- peer: local_peer_id.value,
- app_instance_uuid: local_app_uuid.value,
+ peer: state.local_peer_id.value,
+ app_instance_uuid: state.local_app_uuid.value,
};
- appendMessage(from, to, data.message);
+ state.appendMessage(from, to, data.message);
await fetch("/_api/v1/message_next", {method: "POST"});
} else {
// Check again in 1 second
@@ -94,106 +68,8 @@ async function getMessages() {
}
}
-const form = document.getElementById('send-message-form');
-const input = form.querySelector('input[type=text]');
-
-form.addEventListener('submit', /** @param {SubmitEvent} ev */async ev => {
- ev.preventDefault();
- if (selected_invite.value && local_peer_id.value && local_app_uuid.value) {
- const message = input.value;
- await fetch("/_api/v1/message_send", {
- method: "POST",
- body: JSON.stringify({
- peer: selected_invite.value.peer,
- app_instance_uuid: selected_invite.value.app_instance_uuid,
- message,
- })
- });
- input.value = "";
- const from = {
- peer: local_peer_id.value,
- app_instance_uuid: local_app_uuid.value,
- };
- const to = {
- peer: selected_invite.value.peer,
- app_instance_uuid: selected_invite.value.app_instance_uuid,
- };
- appendMessage(from, to, message);
- }
-});
-
-delegate(document.body, "click", "#peers li", /** @this {HTMLLIElement} */function () {
- selected_invite.value = JSON.parse(this.dataset.invite);
-});
-
-function renderLocalPeerId() {
- const el = document.getElementById('local-peer-id');
- el.textContent = `You: ${local_peer_id.value}`;
-}
-
-function renderSelectedInvite() {
- const json = JSON.stringify(selected_invite.value);
- for (const li of document.querySelectorAll("#peers li")) {
- if (li.dataset.invite === json) {
- li.classList.add("active");
- } else {
- li.classList.remove("active");
- }
- }
- if (selected_invite.value) {
- form.style.display = 'flex';
- input.focus();
- } else {
- form.style.display = 'none';
- }
-}
-
-function renderMessages() {
- const el = document.getElementById('message-history');
- let txt = "";
- if (selected_invite.value) {
- for (const msg of messages.value) {
- const match_from = (
- msg.from.peer === selected_invite.value.peer &&
- msg.from.app_instance_uuid === selected_invite.value.app_instance_uuid
- );
- const match_to = (
- msg.to.peer === selected_invite.value.peer &&
- msg.to.app_instance_uuid === selected_invite.value.app_instance_uuid
- );
- if (match_from || match_to) {
- const from = msg.from.peer === local_peer_id.value ? 'You' : msg.from.peer;
- txt += `<${from}>: ${msg.message}\n`;
- }
- }
- }
- el.textContent = txt;
-}
-
-function renderInvites() {
- const el = document.getElementById('peers');
- el.innerHTML = '';
- if (invites.value.length === 0) {
- const span = document.createElement('span');
- span.textContent = "No peers discovered yet";
- el.appendChild(span);
- } else {
- const ul = document.createElement('ul');
- for (const invite of invites.value) {
- ul.appendChild(renderInvite(invite));
- }
- el.appendChild(ul);
- }
-}
-
-// Update UI when state changes
-watch([selected_invite], renderSelectedInvite);
-watch([local_peer_id], renderLocalPeerId);
-watch([local_peer_id, selected_invite, messages], renderMessages);
-watch([invites], renderInvites);
-
// Initialize example app
-renderInvites();
+// renderInvites();
await updateLocalPeerId();
await updateLocalAppInstance();
await updatePeers();
diff --git a/examples/chat/state.js b/examples/chat/state.js
new file mode 100644
index 0000000..2e0ab3c
--- /dev/null
+++ b/examples/chat/state.js
@@ -0,0 +1,24 @@
+// @ts-check
+import {Signaller} from "./lib/signaller.js";
+
+/** @typedef {{peer: string, uuid: string}} Invite */
+/** @typedef {{peer: string, app_instance_uuid: string}} AppInstance */
+/** @typedef {{message: string, from: AppInstance, to: AppInstance}} Message */
+
+export const local_peer_id = new Signaller(/** @type {null | string} */(null));
+export const local_app_uuid = new Signaller(/** @type {null | string} */(null));
+export const selected_invite = new Signaller(/** @type {null | AppInstance} */(null));
+export const peers = new Signaller(new Set());
+export const messages = new Signaller(/** @type {Message[]} */([]));
+export const invites = new Signaller(/** @type {Invite[]} */([]));
+
+/**
+ * @param {AppInstance} from
+ * @param {AppInstance} to
+ * @param {string} message
+ */
+export function appendMessage(from, to, message) {
+ messages.value.push({from, to, message});
+ messages.signal();
+}
+
diff --git a/examples/chat/style.css b/examples/chat/style.css
index 19c7ec3..1c6e800 100644
--- a/examples/chat/style.css
+++ b/examples/chat/style.css
@@ -27,14 +27,14 @@ main {
flex-grow: 1;
}
-#peers, #messages {
+chat-peers, #messages {
padding: 0;
margin: 0;
box-sizing: border-box;
min-height: 100%;
}
-#peers {
+chat-peers {
padding: 1em;
border-right: 1px solid #ccc;
width: 20%;
@@ -67,7 +67,7 @@ main {
flex-shrink: 1;
}
-#message-history {
+chat-message-history {
flex-grow: 1;
flex-shrink: 1;
padding: 1em;