Skip to main content

host_extensions/first_party/
data.rs

1//! Data connection extension — P2P communication via the host.
2//!
3//! Registers as `window.host.ext.data` in the SPA WebView.
4//! Uses the core `__hostCall` bridge (shared pending-promise map).
5
6use crate::HostExtension;
7
8/// JS injection script for the data extension.
9///
10/// Reuses the core bridge via `window.__hostCall('hostBridge', ...)`.
11/// Push events (dataConnected, dataMessage, dataBinary, dataClosed, dataError,
12/// dataIncomingCall) are dispatched through the core `window.host.on()` / `__hostPush` path.
13const HOST_EXT_DATA_SCRIPT: &str = r#"
14(function(){
15    if (!window.host || !window.host.ext) return;
16    function bytesToBase64(input) {
17        var bytes;
18        if (input instanceof Uint8Array) {
19            bytes = input;
20        } else if (input instanceof ArrayBuffer) {
21            bytes = new Uint8Array(input);
22        } else if (ArrayBuffer.isView(input)) {
23            bytes = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
24        } else {
25            throw new TypeError('sendBytes expects a Uint8Array or ArrayBuffer');
26        }
27
28        var chunkSize = 0x8000;
29        var binary = '';
30        for (var i = 0; i < bytes.length; i += chunkSize) {
31            var chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length));
32            binary += String.fromCharCode.apply(null, chunk);
33        }
34        return btoa(binary);
35    }
36
37    window.host.ext.data = Object.freeze({
38        getPeerId: function() {
39            return window.__hostCall('hostBridge', 'dataGetPeerId', {});
40        },
41        connect: function(peerAddress) {
42            return window.__hostCall('hostBridge', 'dataConnect', { peerAddress: peerAddress });
43        },
44        send: function(connId, data) {
45            return window.__hostCall('hostBridge', 'dataSend', { connId: connId, data: data });
46        },
47        sendBytes: function(connId, data) {
48            return window.__hostCall('hostBridge', 'dataSendBytes', {
49                connId: connId,
50                dataBase64: bytesToBase64(data)
51            });
52        },
53        close: function(connId) {
54            return window.__hostCall('hostBridge', 'dataClose', { connId: connId });
55        },
56        startListening: function(address) {
57            return window.__hostCall('hostBridge', 'dataStartListening', { address: address });
58        }
59    });
60})();
61"#;
62
63pub struct DataExtension;
64
65impl Default for DataExtension {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl DataExtension {
72    pub fn new() -> Self {
73        Self
74    }
75}
76
77impl HostExtension for DataExtension {
78    fn namespace(&self) -> &str {
79        "data"
80    }
81
82    fn channel(&self) -> &str {
83        // Reuses the core hostBridge channel — no separate handler needed.
84        "hostBridge"
85    }
86
87    fn inject_script(&self) -> &str {
88        HOST_EXT_DATA_SCRIPT
89    }
90
91    fn handle_message(&self, _method: &str, _params: &str) -> Option<String> {
92        // Messages are handled by the core js_bridge dispatcher —
93        // this extension only contributes the JS injection.
94        None
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn data_extension_basics() {
104        let ext = DataExtension::new();
105        assert_eq!(ext.namespace(), "data");
106        assert!(ext.inject_script().contains("window.host.ext.data"));
107        assert!(ext.inject_script().contains("dataConnect"));
108        assert!(ext.inject_script().contains("sendBytes"));
109    }
110}