-
-
Notifications
You must be signed in to change notification settings - Fork 68
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Cloudflare Worker-based EthernetProvider
Uses durable objects to route ethernet packets between clients in a zone. Nothing is actually persisted in storage, we use the in-memory state of the durable object, since it only makes sense to broadcast packets while to other clients that are currently actively connected. There is also a debug /zone/<name>/list UI to list currently connected clients. For #53
- Loading branch information
Showing
6 changed files
with
263 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import type { | ||
EmulatorEthernetProvider, | ||
EmulatorEthernetProviderDelegate, | ||
} from "./BasiliskII/emulator-ui"; | ||
|
||
/** | ||
* Works in conjunction with the Clouldflare worker defined in | ||
* workers-ethernet/src/index.mjs to broadcast Ethernet packets to all emulator | ||
* instances in the same named zone. | ||
*/ | ||
export class CloudflareWorkerEthernetProvider | ||
implements EmulatorEthernetProvider | ||
{ | ||
#zoneName: string; | ||
#macAddress?: string; | ||
#webSsocket: WebSocket; | ||
#delegate?: EmulatorEthernetProviderDelegate; | ||
#state: "opening" | "closed" | "opened" = "opening"; | ||
#bufferedMessages: string[] = []; | ||
#reconnectTimeout?: number; | ||
|
||
constructor(zoneName: string) { | ||
this.#zoneName = zoneName; | ||
this.#webSsocket = this.#connect(); | ||
} | ||
|
||
#connect(): WebSocket { | ||
const protocol = location.protocol === "https:" ? "wss:" : "ws:"; | ||
const origin = `${protocol}//${location.host}`; | ||
const webSocket = new WebSocket( | ||
`${origin}/zone/${this.#zoneName}/websocket` | ||
); | ||
webSocket.addEventListener("open", this.#handleOpen); | ||
webSocket.addEventListener("close", this.#handleClose); | ||
webSocket.addEventListener("error", this.#handleError); | ||
webSocket.addEventListener("message", this.#handleMessage); | ||
return webSocket; | ||
} | ||
|
||
#reconnect(): void { | ||
const webSocket = this.#webSsocket; | ||
webSocket.removeEventListener("open", this.#handleOpen); | ||
webSocket.removeEventListener("close", this.#handleClose); | ||
webSocket.removeEventListener("error", this.#handleError); | ||
webSocket.removeEventListener("message", this.#handleMessage); | ||
|
||
this.#state = "opening"; | ||
if (this.#reconnectTimeout) { | ||
window.clearTimeout(this.#reconnectTimeout); | ||
} | ||
this.#reconnectTimeout = window.setTimeout(() => { | ||
this.#webSsocket = this.#connect(); | ||
}, 1000); | ||
} | ||
|
||
init(macAddress: string): void { | ||
this.#macAddress = macAddress; | ||
this.#send({type: "init", macAddress}); | ||
} | ||
|
||
close() { | ||
this.#state = "closed"; | ||
this.#send({type: "close"}); | ||
this.#webSsocket.close(); | ||
} | ||
|
||
send(destination: string, packet: Uint8Array): void { | ||
// TODO: send packets directly as Uint8Arrays, to avoid copying overhead. | ||
this.#send({ | ||
type: "send", | ||
destination, | ||
packetArray: Array.from(packet), | ||
}); | ||
} | ||
|
||
#send(message: any) { | ||
message = JSON.stringify(message); | ||
if (this.#state === "opened") { | ||
this.#webSsocket.send(message); | ||
} else { | ||
this.#bufferedMessages.push(message); | ||
} | ||
} | ||
|
||
setDelegate(delegate: EmulatorEthernetProviderDelegate): void { | ||
this.#delegate = delegate; | ||
} | ||
|
||
#handleOpen = (event: Event): void => { | ||
this.#state = "opened"; | ||
const bufferedMessages = this.#bufferedMessages; | ||
this.#bufferedMessages = []; | ||
for (const message of bufferedMessages) { | ||
this.#webSsocket.send(message); | ||
} | ||
if (this.#macAddress) { | ||
this.init(this.#macAddress); | ||
} | ||
}; | ||
|
||
#handleClose = (event: CloseEvent): void => { | ||
if (this.#state === "closed") { | ||
// Intentionally closed | ||
return; | ||
} | ||
this.#reconnect(); | ||
}; | ||
|
||
#handleError = (event: Event): void => { | ||
console.error("WebSocket error", event); | ||
this.#reconnect(); | ||
}; | ||
|
||
#handleMessage = (event: MessageEvent): void => { | ||
const data = JSON.parse(event.data); | ||
const {type} = data; | ||
switch (type) { | ||
case "receive": | ||
const {packetArray} = data; | ||
const packet = new Uint8Array(packetArray); | ||
this.#delegate?.receive(packet); | ||
break; | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
export class EthernetZone implements DurableObject { | ||
#clients: EthernetZoneClient[] = []; | ||
|
||
async fetch(request: Request) { | ||
const url = new URL(request.url); | ||
if (url.pathname === "/websocket") { | ||
return this.handleWebSocket(request); | ||
} else if (url.pathname === "/list") { | ||
return new Response( | ||
`clients: ${JSON.stringify( | ||
this.#clients, | ||
(key, value) => { | ||
if (key === "webSocket") { | ||
const webSocket = value as WebSocket; | ||
return { | ||
readyState: webSocket.readyState, | ||
url: webSocket.url, | ||
protocol: webSocket.protocol, | ||
}; | ||
} | ||
return value; | ||
}, | ||
2 | ||
)}`, | ||
{ | ||
status: 200, | ||
statusText: "OK", | ||
headers: { | ||
"Content-Type": "text/plain", | ||
}, | ||
} | ||
); | ||
} | ||
return new Response("Not found (EthernetZone)", {status: 404}); | ||
} | ||
|
||
async handleWebSocket(request: Request) { | ||
if (request.headers.get("Upgrade") !== "websocket") { | ||
return new Response("Expected websocket", {status: 400}); | ||
} | ||
|
||
const [client, server] = Object.values(new WebSocketPair()); | ||
|
||
await this.handleClient(server); | ||
|
||
return new Response(null, {status: 101, webSocket: client}); | ||
} | ||
|
||
async handleClient(webSocket: WebSocket) { | ||
webSocket.accept(); | ||
|
||
const client: EthernetZoneClient = {webSocket}; | ||
this.#clients.push(client); | ||
|
||
webSocket.addEventListener("message", async event => { | ||
const data = JSON.parse(event.data as string); | ||
const {type, ...payload} = data; | ||
switch (type) { | ||
case "init": | ||
client.macAddress = payload.macAddress; | ||
break; | ||
case "close": | ||
this.closeClient(client); | ||
break; | ||
case "send": | ||
const {destination, ...sendPayload} = payload; | ||
for (const otherClient of this.#clients) { | ||
if (otherClient === client || otherClient.closed) { | ||
continue; | ||
} | ||
if ( | ||
destination === "*" || | ||
destination === "AT" || | ||
destination === otherClient.macAddress | ||
) { | ||
try { | ||
otherClient.webSocket.send( | ||
JSON.stringify({ | ||
type: "receive", | ||
...sendPayload, | ||
}) | ||
); | ||
} catch (err) { | ||
this.closeClient(otherClient); | ||
} | ||
} | ||
} | ||
break; | ||
default: | ||
console.warn("Unexpected message", data); | ||
} | ||
}); | ||
} | ||
|
||
closeClient(client: EthernetZoneClient) { | ||
client.closed = true; | ||
this.#clients.splice(this.#clients.indexOf(client), 1); | ||
} | ||
} | ||
|
||
type EthernetZoneClient = { | ||
webSocket: WebSocket; | ||
macAddress?: string; | ||
closed?: boolean; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters