char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport web extension to typescript44fe2c6

main
11.2 KiB337 linesraw
1use avril_core::{Extension, Op};
2use std::convert::Infallible;
3
4#[cfg(test)]
5pub fn init() -> Extension {
6    with_broker(std::sync::Arc::default())
7}
8
9pub fn with_broker(broker: std::sync::Arc<avril_host::messaging::Broker>) -> Extension {
10    let metadata = (
11        match std::env::consts::OS {
12            "windows" => "Win32".into(),
13            "macos" => "MacIntel".into(),
14            "linux" => format!("Linux {}", std::env::consts::ARCH),
15            os => os.into(),
16        },
17        std::thread::available_parallelism()
18            .map(|parallelism| parallelism.get())
19            .unwrap_or(1),
20        concat!("avril/", env!("CARGO_PKG_VERSION")),
21    );
22    #[derive(Clone, serde::Serialize)]
23    struct Brand {
24        brand: String,
25        version: String,
26    }
27    #[derive(Clone, serde::Serialize)]
28    #[serde(rename_all = "camelCase")]
29    struct UserAgentData {
30        brands: Vec<Brand>,
31        mobile: bool,
32        platform: String,
33        architecture: String,
34        bitness: String,
35        model: String,
36        platform_version: String,
37        ua_full_version: String,
38        full_version_list: Vec<Brand>,
39        wow64: bool,
40    }
41    const FULL_VERSION: &str = env!("CARGO_PKG_VERSION");
42    let major = FULL_VERSION.split('.').next().unwrap_or(FULL_VERSION);
43    let user_agent_data = UserAgentData {
44        brands: vec![Brand {
45            brand: "avril".into(),
46            version: major.into(),
47        }],
48        mobile: false,
49        platform: match std::env::consts::OS {
50            "windows" => "Windows".into(),
51            "macos" => "macOS".into(),
52            "linux" => "Linux".into(),
53            os => os.into(),
54        },
55        architecture: match std::env::consts::ARCH {
56            "x86" | "i686" | "x86_64" => "x86".into(),
57            "arm" | "aarch64" => "arm".into(),
58            arch => arch.into(),
59        },
60        bitness: match std::env::consts::ARCH {
61            "x86_64" | "aarch64" => "64".into(),
62            _ => "32".into(),
63        },
64        model: String::new(),
65        platform_version: String::new(),
66        ua_full_version: FULL_VERSION.into(),
67        full_version_list: vec![Brand {
68            brand: "avril".into(),
69            version: FULL_VERSION.into(),
70        }],
71        wow64: false,
72    };
73    let mut ops = avril_host::messaging::ops(broker);
74    ops.push(Op::new("op_navigator_metadata", move |(): ()| {
75        Ok::<_, Infallible>(metadata.clone())
76    }));
77    ops.push(Op::new("op_navigator_user_agent_data", move |(): ()| {
78        Ok::<_, Infallible>(user_agent_data.clone())
79    }));
80    Extension {
81        name: "ext:web",
82        dependencies: &["ext:timers"],
83        ops,
84        modules: &[
85            (
86                "ext:web/main.js",
87                include_str!(concat!(env!("OUT_DIR"), "/main.js")),
88            ),
89            (
90                "ext:web/idl.js",
91                include_str!(concat!(env!("OUT_DIR"), "/idl.js")),
92            ),
93            (
94                "ext:web/interface.js",
95                include_str!(concat!(env!("OUT_DIR"), "/interface.js")),
96            ),
97            (
98                "ext:web/events.js",
99                include_str!(concat!(env!("OUT_DIR"), "/events.js")),
100            ),
101            (
102                "ext:web/messaging.js",
103                include_str!(concat!(env!("OUT_DIR"), "/messaging.js")),
104            ),
105            (
106                "ext:web/broadcast.js",
107                include_str!(concat!(env!("OUT_DIR"), "/broadcast.js")),
108            ),
109            (
110                "ext:web/serialization.js",
111                include_str!(concat!(env!("OUT_DIR"), "/serialization.js")),
112            ),
113            (
114                "ext:web/base64.js",
115                include_str!(concat!(env!("OUT_DIR"), "/base64.js")),
116            ),
117            (
118                "ext:web/navigator.js",
119                include_str!(concat!(env!("OUT_DIR"), "/navigator.js")),
120            ),
121            (
122                "ext:web/performance.js",
123                include_str!(concat!(env!("OUT_DIR"), "/performance.js")),
124            ),
125        ],
126        entry_point: Some("ext:web/main.js"),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use avril_core::{Extension, Op, Runtime, RuntimeOptions};
133    use std::convert::Infallible;
134
135    fn runtime() -> Runtime {
136        Runtime::new(RuntimeOptions {
137            extensions: vec![
138                super::init(),
139                avril_ext_timers::init().unwrap(),
140                Extension {
141                    name: "ext:gc-test",
142                    dependencies: &[],
143                    ops: vec![Op::with_scope("collect", |scope, (): ()| {
144                        scope.low_memory_notification();
145                        Ok::<_, Infallible>(())
146                    })],
147                    modules: &[(
148                        "ext:gc-test/main.js",
149                        "import {collect} from 'ext:core/ops'; globalThis.collect=collect;",
150                    )],
151                    entry_point: Some("ext:gc-test/main.js"),
152                },
153            ],
154            ..Default::default()
155        })
156        .unwrap()
157    }
158
159    fn collect(runtime: &mut Runtime) {
160        for _ in 0..4 {
161            runtime.execute_script::<()>("gc", "collect();").unwrap();
162            runtime.run_event_loop().unwrap();
163        }
164    }
165
166    #[test]
167    fn unobserved_channels_and_started_ports_can_be_collected() {
168        let mut runtime = runtime();
169        runtime
170            .execute_script::<()>(
171                "objects",
172                r#"
173            globalThis.cleaned = {channels:0,ports:0,control:0};
174            globalThis.registry = new FinalizationRegistry(kind => cleaned[kind]++);
175            (() => {
176                for (let i=0;i<100;i++) {
177                    registry.register(new BroadcastChannel('gc'), 'channels');
178                    const channel = new MessageChannel();
179                    channel.port1.start();
180                    registry.register(channel.port1, 'ports');
181                    registry.register({}, 'control');
182                }
183            })();
184        "#,
185            )
186            .unwrap();
187        collect(&mut runtime);
188        assert!(
189            runtime
190                .execute_script::<bool>(
191                    "counts",
192                    "cleaned.channels > 50 && cleaned.ports > 50 && cleaned.control > 50"
193                )
194                .unwrap()
195        );
196    }
197
198    #[test]
199    fn channel_listener_lifetimes_include_once_abort_and_borrowed_methods() {
200        let mut runtime = runtime();
201        runtime
202            .execute_script::<()>(
203                "objects",
204                r#"
205            globalThis.cleaned = [];
206            globalThis.registry = new FinalizationRegistry(kind => cleaned.push(kind));
207            globalThis.sender = new BroadcastChannel('lifetime');
208            globalThis.controller = new AbortController();
209            (() => {
210                const channel = new BroadcastChannel('lifetime');
211                channel.onmessage = () => {};
212                registry.register(channel,'handler');
213                globalThis.weak = new WeakRef(channel);
214                const once = new BroadcastChannel('lifetime');
215                EventTarget.prototype.addEventListener.call(once,'message',()=>{}, {once:true});
216                registry.register(once,'once');
217                const aborted = new BroadcastChannel('lifetime');
218                aborted.addEventListener('message',()=>{}, {signal:controller.signal});
219                registry.register(aborted,'abort');
220            })();
221        "#,
222            )
223            .unwrap();
224        collect(&mut runtime);
225        assert!(
226            runtime
227                .execute_script::<bool>(
228                    "alive",
229                    "weak.deref() !== undefined && cleaned.length === 0"
230                )
231                .unwrap()
232        );
233        runtime
234            .execute_script::<()>(
235                "release",
236                r#"
237            weak.deref().onmessage = null;
238            controller.abort();
239            sender.postMessage('event');
240        "#,
241            )
242            .unwrap();
243        runtime.run_event_loop().unwrap();
244        collect(&mut runtime);
245        assert!(
246            runtime
247                .execute_script::<bool>(
248                    "released",
249                    "['handler','once','abort'].every(kind => cleaned.includes(kind))"
250                )
251                .unwrap()
252        );
253        runtime
254            .execute_script::<()>("close", "sender.close();")
255            .unwrap();
256    }
257
258    #[test]
259    fn collection_can_race_pending_port_deliveries() {
260        let mut runtime = runtime();
261        runtime
262            .execute_script::<()>(
263                "deliveries",
264                r#"
265            globalThis.cleaned = 0;
266            globalThis.registry = new FinalizationRegistry(()=>cleaned++);
267            globalThis.done = false;
268            (async()=> {
269                const peers = [];
270                for (let round=0;round<10;round++) {
271                    (()=> {
272                        for (let i=0;i<100;i++) {
273                            const channel = new MessageChannel();
274                            if (i % 2) {
275                                channel.port1.onmessage = ()=>{};
276                                channel.port1.onmessage = null;
277                            } else channel.port1.start();
278                            registry.register(channel.port1,0);
279                            channel.port2.postMessage({data:new Uint8Array(64)});
280                            peers.push(channel.port2);
281                        }
282                    })();
283                    await new Promise(resolve=>setTimeout(()=>{collect();resolve();},0));
284                    collect();
285                }
286                peers.forEach(peer=>peer.close());
287                done = true;
288            })();
289        "#,
290            )
291            .unwrap();
292        runtime.run_event_loop().unwrap();
293        collect(&mut runtime);
294        assert!(
295            runtime
296                .execute_script::<bool>("done", "done && cleaned > 500")
297                .unwrap()
298        );
299    }
300
301    #[test]
302    fn ports_with_listeners_stay_alive_until_the_peer_closes() {
303        let mut runtime = runtime();
304        runtime
305            .execute_script::<()>(
306                "objects",
307                r#"
308            globalThis.cleaned = false;
309            globalThis.registry = new FinalizationRegistry(()=> cleaned=true);
310            (() => {
311                const channel = new MessageChannel();
312                channel.port1.onmessage = ()=> {};
313                registry.register(channel.port1,0);
314                globalThis.weak = new WeakRef(channel.port1);
315                globalThis.peer = channel.port2;
316            })();
317        "#,
318            )
319            .unwrap();
320        collect(&mut runtime);
321        assert!(
322            runtime
323                .execute_script::<bool>("alive", "weak.deref() !== undefined && !cleaned")
324                .unwrap()
325        );
326        runtime
327            .execute_script::<()>("close", "peer.close();")
328            .unwrap();
329        runtime.run_event_loop().unwrap();
330        collect(&mut runtime);
331        assert!(
332            runtime
333                .execute_script::<bool>("released", "cleaned")
334                .unwrap()
335        );
336    }
337}