vail

Internet morse code repeater
git clone https://git.woozle.org/neale/vail.git

vail / static
Neale Pickett  ·  2024-10-24

api-demo.html

  1<!DOCTYPE html>
  2<html>
  3	<head>
  4		<title>Vail API Demo</title>
  5		<style>
  6body {
  7	font-family: Arimo, Arial, monospace;
  8}
  9
 10section {
 11	border: 1px solid black;
 12	margin: 1em;
 13}
 14
 15p {
 16	max-width: 40em;
 17}
 18
 19.log {
 20	max-height: 10em;
 21	overflow: scroll;
 22}
 23		</style>
 24		<script type="module">
 25const Millisecond = 1
 26const Second = Millisecond * 1000
 27
 28/** Vail server connection.
 29 *
 30 * This opens a websocket to the given repeater on vail.woozle.org,
 31 * and dispatches events to itself. Use .addEventListener to set up
 32 * event handlers as you desire.
 33 *
 34 * Events:
 35 *    clients: the number of connected clients has changed
 36 *    message: a message was recieved
 37 *    sound: your application should start making a sound
 38 *    nosound: your application should stop making a sound
 39 *
 40 * Multiple sequential sound events may be dispatched before a nosound event is.
 41 * This indicates "cross-talk": two people are sending to the repeater at the
 42 * same time.
 43 *
 44 * Class variables you may enjoy:
 45 *
 46 *   delay: rx delay to add to incoming messages, higher-latency networks will need a larger value
 47 *   transmitters: how many clients are sounding
 48 *   reconnect: if true, will try to maintain a connection to the server
 49 *   reconnectDelay: how long to wait between disconnection and reconnect attempt
 50 *   clients: how many connected clients the repeater last reported
 51 *   offset: clock skew between us and the repeater
 52 */
 53class Vail extends EventTarget {
 54	constructor(repeater) {
 55		super()
 56
 57		/** URL to our WebSocket */
 58		this.url = new URL("wss://vail.woozle.org/chat")
 59		this.url.searchParams.set("repeater", repeater)
 60
 61		/** Delay to add */
 62		this.delay = 4 * Second
 63
 64		/** How many things are making sound right now. 0 == be quiet */
 65		this.transmitters = 0
 66
 67		/** Attempt to reconnect if the websocket closes */
 68		this.reconnect = true
 69
 70		/** How long to wait before trying to reconnect */
 71		this.reconnectDelay = 3 * Second
 72
 73		this.reopen()
 74	}
 75
 76	/** Close the web socket and stop trying to reconnect */
 77	close() {
 78		this.reconnect = false
 79		if (!this.socket) {
 80			return
 81		}
 82		this.socket.close()
 83	}
 84
 85	reopen() {
 86		/** Number of clients connected */
 87		this.clients = 0
 88
 89		/** Timestamp offset for incoming messages */
 90		this.offset = 0
 91
 92		/** Our current websocket */
 93		this.socket = new WebSocket(this.url, ["json.vail.woozle.org"])
 94		this.socket.addEventListener("close", event => {
 95			addTimeout(() => this.reopen(), this.reconnectDelay)
 96		})
 97		this.socket.addEventListener("message", event => {
 98			this.message(event)
 99		})
100	}
101
102	message(event) {
103		let message = JSON.parse(event.data)
104		this.dispatchEvent(new CustomEvent("message", {detail: {message}}))
105		if (message.Clients != this.clients) {
106			this.clients = message.Clients
107			this.dispatchEvent(new CustomEvent("clients", {detail: {clients: this.clients}}))
108		}
109		if (message.Duration.length == 0) {
110			this.offset = Date.now() - message.Timestamp
111			return
112		}
113
114		// Defer dispatching sound events
115		let now = Date.now()
116		let when = message.Timestamp + this.offset + this.delay
117		let tx = true
118		let detail = {}
119		for (let duration of message.Duration) {
120			let delay = when - now
121			if (tx && (delay >= 0)) {
122				setTimeout(() => this.sound(true, {when, duration}), delay)
123				setTimeout(() => this.sound(false, {when: when+duration, duration}), delay + duration)
124			}
125			when += duration
126			tx = !tx
127		}
128	}
129
130	/** Dispatch a sound event.
131	 *
132	 * This keeps an internal count of how many things are making sound at once.
133	 * When that count reaches 0, a "nosound" event is dispatched.
134	 *
135	 * @param {bool} tx True to make sound, false to stop making sound.
136	 * @param {Object} detail CustomEvent detail
137	 */
138	sound(tx, detail) {
139		if (tx) {
140			this.transmitters++
141		} else {
142			this.transmitters--
143		}
144		detail.transmitters = this.transmitters
145
146		if (this.transmitters > 0) {
147			this.dispatchEvent(new CustomEvent("sound", {detail}))
148		} else {
149			this.dispatchEvent(new CustomEvent("nosound", {detail}))
150		}
151	}
152}
153
154/** Log an event's detail */
155function log(selector, event) {
156	let section = document.querySelector(selector)
157	let log = section.querySelector(".log")
158	let line = log.appendChild(document.createElement("div"))
159	line.textContent = event.type + ": " + JSON.stringify(event.detail)
160	line.scrollIntoView()
161}
162
163/** Update the displayed number of clients. */
164function clients(event) {
165	let clients = document.querySelector("#nclients")
166	clients.textContent = event.target.clients
167}
168
169/** Handle the sound and nosound events by displaying different glyphs. */
170function sound(event, enable) {
171	let sounder = document.querySelector("#sounder")
172	sounder.textContent = enable ? "█" : ""
173}
174
175let vail = new Vail("demo") // Connect to the "demo" repeater
176vail.addEventListener("message", event => log("#messages", event))
177vail.addEventListener("clients", event => log("#clients", event))
178vail.addEventListener("sound", event => log("#sounds", event))
179vail.addEventListener("nosound", event => log("#sounds", event))
180
181vail.addEventListener("clients", event => clients(event))
182vail.addEventListener("sound", event => sound(event, true))
183vail.addEventListener("nosound", event => sound(event, false))
184		</script>
185	</head>
186	<body>
187		<h1>Vail API Demo</h1>
188
189		<div><a href="https://vail.woozle.org/#demo" target="_blank">Repeater</a></div>
190		<div>Sounder: <span id="sounder"></span></div>
191		<div>Clients: <span id="nclients">0</span></div>
192
193		<section id="sounds">
194			<h2>sound events</h2>
195			<div class="log"></div>
196			<p>
197				Implementations should use sound events, which will be dispatched
198				in the correct order, and only have one duration per event.
199			</p>
200		</section>
201
202		<section id="clients">
203			<h2>clients events</h2>
204			<div class="log"></div>
205			<p>
206				Clients events are dispatched whenever the number of
207				connected clients changes.
208			</p>
209		</section>
210
211		<section id="messages">
212			<h2>message events</h2>
213			<div class="log"></div>
214			<p>
215				Message events are "raw" events. They may arrive out of order,
216				and can have multiple durations.
217				The Vail class parses these into properly-sequenced
218				"sound" events.
219			</p>
220		</section>
221	</body>
222</html>