From 2cee0768f397d0ba4e989096cada068e1f4b1602 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 11 Nov 2023 02:08:31 +0100 Subject: [PATCH] implement dxb over http, goodbye messages, improve endpoint online detection --- compiler/compiler.ts | 2 +- compiler/protocol_types.ts | 1 + network/datex-http-channel.ts | 25 +++++++++++++++++++++++++ network/supranet.ts | 7 +++++++ runtime/runtime.ts | 34 ++++++++++++++++++++++++++++++---- types/addressing.ts | 11 +++++++++++ utils/debug-cookie.ts | 1 + 7 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 network/datex-http-channel.ts diff --git a/compiler/compiler.ts b/compiler/compiler.ts index 8517fcc9..b80a8ae8 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -79,7 +79,7 @@ export class DatexResponse { export const ProtocolDataTypesMap = [ - "REQUEST", "RESPONSE", "DATA", "TMP_SCOPE", "LOCAL", "HELLO", "DEBUGGER", "SOURCE_MAP", "UPDATE" + "REQUEST", "RESPONSE", "DATA", "TMP_SCOPE", "LOCAL", "HELLO", "DEBUGGER", "SOURCE_MAP", "UPDATE", "GOODBYE" ] diff --git a/compiler/protocol_types.ts b/compiler/protocol_types.ts index 25dd1eee..ded80ed1 100644 --- a/compiler/protocol_types.ts +++ b/compiler/protocol_types.ts @@ -11,4 +11,5 @@ export enum ProtocolDataType { DEBUGGER = 6, // get a debugger for a scope SOURCE_MAP = 7, // send a source map for a scope UPDATE = 8, // like normal request, but don't propgate updated pointer values back to sender (prevent recursive loop) + GOODBYE = 9, // info message that endpoint is offline } \ No newline at end of file diff --git a/network/datex-http-channel.ts b/network/datex-http-channel.ts new file mode 100644 index 00000000..4e1159bf --- /dev/null +++ b/network/datex-http-channel.ts @@ -0,0 +1,25 @@ +/** + * Send DATEX blocks to a server with a HTTPComInterface + * (listening to POST requests on /datex-http). + * No response is returned. + * + * This is not the same as datex-over-http implemented in UIX + * that send unsigned/unencrypted DATEX scripts to an HTTP server + */ +export function sendDatexViaHTTPChannel(dxb: ArrayBuffer, origin = window.location.origin) { + + // fallback to sendBeacon for firefox until fetch keepalive implemented + if (navigator.userAgent.includes("Firefox/")) { + navigator.sendBeacon(origin + "/datex-http", dxb); + } + else { + fetch(origin + "/datex-http", { + method:'post', + headers:{ + 'Content-Type': 'application/datex', + }, + body: dxb, + keepalive: true + }) + } +} \ No newline at end of file diff --git a/network/supranet.ts b/network/supranet.ts index 152cd385..0454e7a6 100644 --- a/network/supranet.ts +++ b/network/supranet.ts @@ -25,6 +25,7 @@ import { endpoint_config } from "../runtime/endpoint_config.ts"; import { endpoint_name, UnresolvedEndpointProperty } from "../datex_all.ts"; import { Datex } from "../mod.ts"; import { Storage } from "../runtime/storage.ts"; +import { sendDatexViaHTTPChannel } from "./datex-http-channel.ts"; const logger = new Logger("DATEX Supranet"); // entry point to connect to the datex network @@ -152,6 +153,12 @@ export class Supranet { // TODO: (does not work because response never reaches endpoint if valid endpoint already exists in network) // Crypto.validateOwnKeysAgainstNetwork(); + // send goodbye on process close + const byeDatex = await Datex.Compiler.compile("", [], {type:Datex.ProtocolDataType.GOODBYE, sign:true, flood:true, __routing_ttl:10}) + globalThis.addEventListener("beforeunload", () => { + sendDatexViaHTTPChannel(byeDatex); + }) + return connected; } diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 48cf23ee..28ddabf1 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -1796,6 +1796,10 @@ export class Runtime { console.log("Scope error occured, cannot get the original error here!"); return; } + + // assume sender endpoint is online now + if (header.sender && header.signed) header.sender.setOnline(true) + // return error to sender (if request) if (header.type == ProtocolDataType.REQUEST) { // is not a DatexError -> convert to DatexError @@ -1839,6 +1843,9 @@ export class Runtime { const unique_sid = header.sid+"-"+header.return_index; + // assume sender endpoint is online now + if (header.sender && header.signed) header.sender.setOnline(true) + // return global result to sender (if request) if (header.type == ProtocolDataType.REQUEST) { this.datexOut(["?", [return_value], {type:ProtocolDataType.RESPONSE, to:header.sender, return_index:header.return_index, encrypt:header.encrypted, sign:header.signed}], header.sender, header.sid, false); @@ -1871,19 +1878,38 @@ export class Runtime { // hello (also temp: get public keys) else if (header.type == ProtocolDataType.HELLO) { - if (return_value) { + if (!header.sender) logger.error("Invalid HELLO message, no sender"); + else if (return_value) { try { - let keys_updated = await Crypto.bindKeys(header.sender, ...<[ArrayBuffer,ArrayBuffer]>return_value); + const keys_updated = await Crypto.bindKeys(header.sender, ...<[ArrayBuffer,ArrayBuffer]>return_value); if (header.routing?.ttl) header.routing.ttl--; + + // set online to true (even if HELLO message not signed) + header.sender.setOnline(true) logger.debug("HELLO ("+header.sid+"/" + header.inc+ "/" + header.return_index + ") from " + header.sender + ", keys "+(keys_updated?"":"not ")+"updated, ttl = " + header.routing?.ttl); } catch (e) { logger.error("Invalid HELLO keys"); } } - else logger.debug("HELLO from " + header.sender + ", no keys, ttl = " + header.routing?.ttl); + else { + // set online to true (even if HELLO message not signed) + header.sender.setOnline(true) + logger.debug("HELLO from " + header.sender + ", no keys, ttl = " + header.routing?.ttl); + } } + + else if (header.type == ProtocolDataType.GOODBYE) { + if (header.signed && header.sender) { + logger.debug("GOODBYE from " + header.sender) + header.sender.setOnline(false) + } + else { + logger.error("ignoring unsigned GOODBYE message") + } + } + else if (header.type == ProtocolDataType.DEBUGGER) { logger.success("DEBUGGER ?", return_value) @@ -5015,7 +5041,7 @@ export class Runtime { catch {} }).filter(o => o && !(o.equals(Runtime.endpoint) || o.equals(SCOPE.sender)))) ]; - if (origins.length > 2) { + if (origins.length > 1) { logger.debug("pre-fetching online state for "+ origins.length + " endpoints") await Promise.race([ Promise.all(origins.map(origin => origin.isOnline())), diff --git a/types/addressing.ts b/types/addressing.ts index ea916372..85b7f653 100644 --- a/types/addressing.ts +++ b/types/addressing.ts @@ -341,6 +341,17 @@ export class Endpoint extends Target { this.#onlinePointer.onGargabeCollection(()=> clearInterval(interval)); return this.#onlinePointer; } + + /** + * Override online state (e.g. when retrieving a GOODBYE or other message) + * @param online + */ + public setOnline(online = true) { + this.#online = new Promise(resolve => resolve(online)); + this.#current_online = online; + if (this.#onlinePointer) this.#onlinePointer.val = online; + } + // returns (cached) online status public async isOnline(): Promise { if (Runtime.endpoint.equals(this) || Runtime.main_node?.equals(this) || this as Endpoint === LOCAL_ENDPOINT) return true; // is own endpoint or main node diff --git a/utils/debug-cookie.ts b/utils/debug-cookie.ts index c6a4238b..e8336573 100644 --- a/utils/debug-cookie.ts +++ b/utils/debug-cookie.ts @@ -8,6 +8,7 @@ export function debugMode(enable = true) { console.log("[debug mode " + (enable ? "enabled" : "disabled") + "]"); if (enable) setCookie("datex-debug", "true") else setCookie("datex-debug", ""); + window.location.reload() }