diff --git a/API-STATUS.md b/API-STATUS.md index e430a6ce38..e3a7ccb5e5 100644 --- a/API-STATUS.md +++ b/API-STATUS.md @@ -64,7 +64,7 @@ This document lists standard JavaScript/Web APIs and their support status in js_ |-----|------|-----|---------------------| | Web Workers | Yes | Yes | `Worker` · Brr: `Brr_webworkers.Worker` | | Service Workers | Yes | Yes | `ServiceWorker` · Brr: `Brr_webworkers.Service_worker` | -| Shared Workers | No | Yes | Brr: `Brr_webworkers.Worker.Shared` | +| Shared Workers | Yes | Yes | `SharedWorker` · Brr: `Brr_webworkers.Worker.Shared` | ## File & Binary Data @@ -156,7 +156,7 @@ This document lists standard JavaScript/Web APIs and their support status in js_ | Web Components (Custom Elements, Shadow DOM) | Partial | No | `Dom_html` (Shadow DOM — `attachShadow`, `shadowRoot`, `assignedSlot`, `slot`); Custom Elements not bound | | Web Crypto API | Yes | Yes | `Crypto` · Brr: `Brr_webcrypto` | | Notifications API | Yes | Yes | `Notification` · Brr: `Brr_io.Notification` | -| Broadcast Channel API | No | Yes | Brr: `Brr_io.Message.Broadcast_channel` | +| Broadcast Channel API | Yes | Yes | `BroadcastChannel` · Brr: `Brr_io.Message.Broadcast_channel` | | AbortController / AbortSignal | Yes | Yes | `Abort` · Brr: `Brr.Abort` | --- @@ -180,11 +180,9 @@ Worker/iframe/window communication. | API | Issue | In Brr | Why | |-----|-------|--------|-----| -| Broadcast Channel API | — | Yes | Cross-tab communication (sync auth state, shared data). Simple API. | | Web Audio API | — | Yes | Audio processing, games, music apps. Large API surface but well-defined. | | Media Capture (getUserMedia) | — | Yes | Video calls, camera/mic access. Growing use with remote work tooling. | | Encoding API (TextEncoder/TextDecoder) | — | Partial | Needed for binary protocol work and streaming text. Small surface. | -| Shared Workers | — | Yes | Shared state across tabs. Niche but Brr covers it. | | IndexedDB | — | No | Client-side database for offline apps. Large API but important for data-heavy PWAs. | | Streams API | — | No | Modern data processing. Fetch response bodies are ReadableStreams. Increasingly foundational. | | History API (upgrade to full) | — | Yes | SPA routing depends on pushState/replaceState. Current binding is limited. | @@ -205,5 +203,5 @@ Worker/iframe/window communication. ### Suggested implementation order -1. **Broadcast Channel** — small API that fills out the - remaining communication gaps. +1. **Encoding API** — small surface, needed for binary protocol + work and streaming text. diff --git a/CHANGES.md b/CHANGES.md index b5ba731b89..8336db26e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ # dev ## Features/Changes +* Lib: add `BroadcastChannel` and `SharedWorker` — bindings to the Broadcast + Channel API and to shared workers (the page-side `SharedWorker` constructors + with their options, and the worker-side `SharedWorkerGlobalScope` with a + `set_onconnect` helper). The wasm runtime no longer loses `connect` events + received while the module is being instantiated (#2400) * Lib: add `WebGL2` — bindings to the WebGL2 rendering context. The context inherits every method and constant of `WebGL`, and adds the WebGL2 objects (vertex array objects, queries, samplers, syncs, transform feedback), 3D and diff --git a/lib/js_of_ocaml/broadcastChannel.ml b/lib/js_of_ocaml/broadcastChannel.ml new file mode 100644 index 0000000000..045ef8ff37 --- /dev/null +++ b/lib/js_of_ocaml/broadcastChannel.ml @@ -0,0 +1,42 @@ +(* Js_of_ocaml library + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +open! Import + +class type broadcastChannel = object ('self) + inherit Dom_html.eventTarget + + method name : Js.js_string Js.t Js.readonly_prop + + method postMessage : 'a. 'a -> unit Js.meth + + method close : unit Js.meth + + method onmessage : + ('self Js.t, ('self, Js.Unsafe.any) Dom_html.messageEvent Js.t) Dom.event_listener + Js.writeonly_prop + + method onmessageerror : + ('self Js.t, ('self, Js.Unsafe.any) Dom_html.messageEvent Js.t) Dom.event_listener + Js.writeonly_prop +end + +let broadcastChannel : (Js.js_string Js.t -> broadcastChannel Js.t) Js.constr = + Js.Unsafe.global##._BroadcastChannel + +let is_supported () = Js.Optdef.test Js.Unsafe.global##._BroadcastChannel diff --git a/lib/js_of_ocaml/broadcastChannel.mli b/lib/js_of_ocaml/broadcastChannel.mli new file mode 100644 index 0000000000..44ae0f76b3 --- /dev/null +++ b/lib/js_of_ocaml/broadcastChannel.mli @@ -0,0 +1,56 @@ +(* Js_of_ocaml library + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +(** Broadcast Channel API. + + A [BroadcastChannel] delivers every posted message to all other channels + with the same name in the same origin (other tabs, windows, workers). + + @see + @see *) + +open Js + +class type broadcastChannel = object ('self) + inherit Dom_html.eventTarget + + method name : js_string t readonly_prop + (** The channel name given to the constructor. *) + + method postMessage : 'a. 'a -> unit meth + (** [chan##postMessage msg] delivers a structured clone of [msg] to every + other channel with the same name in the same origin. Raises once the + channel has been closed. *) + + method close : unit meth + + method onmessage : + ('self t, ('self, Unsafe.any) Dom_html.messageEvent t) Dom.event_listener + writeonly_prop + + method onmessageerror : + ('self t, ('self, Unsafe.any) Dom_html.messageEvent t) Dom.event_listener + writeonly_prop +end + +val broadcastChannel : (js_string t -> broadcastChannel t) constr +(** [new%js broadcastChannel name] joins the broadcast channel [name]. *) + +val is_supported : unit -> bool +(** Whether the [BroadcastChannel] global is available in the current + environment. *) diff --git a/lib/js_of_ocaml/js_of_ocaml.ml b/lib/js_of_ocaml/js_of_ocaml.ml index d9eb735406..8d9b753731 100644 --- a/lib/js_of_ocaml/js_of_ocaml.ml +++ b/lib/js_of_ocaml/js_of_ocaml.ml @@ -22,6 +22,7 @@ {b This library is provided by the [js_of_ocaml] opam package.} *) module Abort = Abort +module BroadcastChannel = BroadcastChannel module CSS = CSS module Cache = Cache module Clipboard = Clipboard @@ -55,6 +56,7 @@ module Promise = Promise module ResizeObserver = ResizeObserver module Regexp = Regexp module ServiceWorker = ServiceWorker +module SharedWorker = SharedWorker module Sys_js = Sys_js module Typed_array = Typed_array module Url = Url diff --git a/lib/js_of_ocaml/sharedWorker.ml b/lib/js_of_ocaml/sharedWorker.ml new file mode 100644 index 0000000000..733aaec581 --- /dev/null +++ b/lib/js_of_ocaml/sharedWorker.ml @@ -0,0 +1,77 @@ +(* Js_of_ocaml library + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +open! Import + +class type sharedWorker = object ('self) + inherit Dom_html.eventTarget + + method port : MessageChannel.messagePort Js.t Js.readonly_prop + + method onerror : + ('self Js.t, Worker.errorEvent Js.t) Dom.event_listener Js.writeonly_prop +end + +class type workerOptions = object + method name : Js.js_string Js.t Js.writeonly_prop + + method _type : Js.js_string Js.t Js.writeonly_prop + + method credentials : Js.js_string Js.t Js.writeonly_prop +end + +let empty_worker_options () : workerOptions Js.t = Js.Unsafe.obj [||] + +let sharedWorker : (Js.js_string Js.t -> sharedWorker Js.t) Js.constr = + Js.Unsafe.global##._SharedWorker + +let sharedWorker_withName : + (Js.js_string Js.t -> Js.js_string Js.t -> sharedWorker Js.t) Js.constr = + Js.Unsafe.global##._SharedWorker + +let sharedWorker_withOptions : + (Js.js_string Js.t -> workerOptions Js.t -> sharedWorker Js.t) Js.constr = + Js.Unsafe.global##._SharedWorker + +let is_supported () = Js.Optdef.test Js.Unsafe.global##._SharedWorker + +class type sharedWorkerGlobalScope = object ('self) + inherit Dom_html.eventTarget + + method name : Js.js_string Js.t Js.readonly_prop + + method close : unit Js.meth + + method onconnect : + ('self Js.t, ('self, Js.Unsafe.any) Dom_html.messageEvent Js.t) Dom.event_listener + Js.writeonly_prop +end + +let global () : sharedWorkerGlobalScope Js.t = Js.Unsafe.global + +let set_onconnect handler = + if not (Js.Optdef.test Js.Unsafe.global##.onconnect) + then invalid_arg "SharedWorker.set_onconnect: onconnect is undefined"; + let js_handler (ev : (_, _) Dom_html.messageEvent Js.t) = + let port : MessageChannel.messagePort Js.t = + Js.Unsafe.coerce + (Js.Optdef.get (Js.array_get ev##.ports 0) (fun () -> assert false)) + in + handler port + in + Js.Unsafe.global##.onconnect := Js.wrap_callback js_handler diff --git a/lib/js_of_ocaml/sharedWorker.mli b/lib/js_of_ocaml/sharedWorker.mli new file mode 100644 index 0000000000..5723470bb4 --- /dev/null +++ b/lib/js_of_ocaml/sharedWorker.mli @@ -0,0 +1,104 @@ +(* Js_of_ocaml library + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +(** Shared Workers. + + A [SharedWorker] is a worker shared by every browsing context of the same + origin that creates one with the same script URL and name. Each context + communicates with the worker through the {!MessageChannel.messagePort} + exposed as {!class-type:sharedWorker}'s [port]; inside the worker, each new + connection is delivered as a [connect] event carrying the other end of that + port. + + @see + @see *) + +open Js + +(** {2 Page side} *) + +class type sharedWorker = object ('self) + inherit Dom_html.eventTarget + + method port : MessageChannel.messagePort t readonly_prop + (** The port used to communicate with the worker. When messages are received + through the [onmessage] property the port is started implicitly; + otherwise call [port##start] to begin receiving. *) + + method onerror : ('self t, Worker.errorEvent t) Dom.event_listener writeonly_prop +end + +(** Options for the {!sharedWorker_withOptions} constructor. All fields are + optional; create an empty record with {!empty_worker_options}. *) +class type workerOptions = object + method name : js_string t writeonly_prop + (** Identifies the worker: contexts constructing a [SharedWorker] with the + same URL and name share a single worker instance. *) + + method _type : js_string t writeonly_prop + (** Either ["classic"] (default) or ["module"]. *) + + method credentials : js_string t writeonly_prop + (** One of ["omit"], ["same-origin"] (default) or ["include"]. Only used + with module workers. *) +end + +val empty_worker_options : unit -> workerOptions t + +val sharedWorker : (js_string t -> sharedWorker t) constr +(** [new%js sharedWorker url] connects to the shared worker running the script + at [url], starting it if it is not running yet. *) + +val sharedWorker_withName : (js_string t -> js_string t -> sharedWorker t) constr +(** [new%js sharedWorker_withName url name] connects to the shared worker + identified by [url] and [name]. *) + +val sharedWorker_withOptions : (js_string t -> workerOptions t -> sharedWorker t) constr + +val is_supported : unit -> bool +(** Whether the [SharedWorker] global is available in the current + environment. *) + +(** {2 Worker side} *) + +(** The global scope of a running shared worker (its [self]). Only meaningful + when the current code is actually running as a shared worker. *) +class type sharedWorkerGlobalScope = object ('self) + inherit Dom_html.eventTarget + + method name : js_string t readonly_prop + (** The name given at construction, or the empty string. *) + + method close : unit meth + + method onconnect : + ('self t, ('self, Unsafe.any) Dom_html.messageEvent t) Dom.event_listener + writeonly_prop + (** Fired each time a new context connects. The first element of the event's + [ports] is the {!MessageChannel.messagePort} to that context. *) +end + +val global : unit -> sharedWorkerGlobalScope t +(** The shared worker's global scope. *) + +val set_onconnect : (MessageChannel.messagePort t -> unit) -> unit +(** [set_onconnect f] calls [f port] each time a new context connects to the + worker, where [port] is the port to that context (already extracted from + the [connect] event). To receive its messages, set [port##.onmessage] or + add a [message] listener and call [port##start]. Raises [Invalid_argument] + when not running in a shared worker. *) diff --git a/lib/tests-browser/dune b/lib/tests-browser/dune index cc867f0fb7..a4cb9d27ff 100644 --- a/lib/tests-browser/dune +++ b/lib/tests-browser/dune @@ -1,5 +1,11 @@ (executables - (names test_beforeunload test_wheel test_fetch test_font_face) + (names + test_beforeunload + test_wheel + test_fetch + test_font_face + test_shared_worker + test_shared_worker_worker) (libraries js_of_ocaml) (modes js wasm) (preprocess @@ -17,6 +23,9 @@ test_fetch.html test_font_face.bc.js test_font_face.html + test_shared_worker.bc.js + test_shared_worker_worker.bc.js + test_shared_worker.html BebasNeue-Regular.ttf)) (alias @@ -31,4 +40,7 @@ test_fetch.html test_font_face.bc.wasm.js test_font_face.html + test_shared_worker.bc.wasm.js + test_shared_worker_worker.bc.wasm.js + test_shared_worker.html BebasNeue-Regular.ttf)) diff --git a/lib/tests-browser/index.html b/lib/tests-browser/index.html index ef7176e5ea..94de2d986c 100644 --- a/lib/tests-browser/index.html +++ b/lib/tests-browser/index.html @@ -28,6 +28,10 @@

Browser tests

test_font_face (wasm) +
  • + test_shared_worker + (wasm) +
  • diff --git a/lib/tests-browser/test_shared_worker.html b/lib/tests-browser/test_shared_worker.html new file mode 100644 index 0000000000..b2d3e2df21 --- /dev/null +++ b/lib/tests-browser/test_shared_worker.html @@ -0,0 +1,62 @@ + + + + + + test_shared_worker + + + + + +

    SharedWorker and BroadcastChannel test

    +

    + Open this page in several tabs. All tabs share a single worker: it + numbers connections, echoes messages with a shared counter, and announces + new connections on the broadcast channel. Broadcast messages are only + delivered to other tabs. +

    +

    + Mode: + — +

    +

    + + +

    +
    + + + diff --git a/lib/tests-browser/test_shared_worker.ml b/lib/tests-browser/test_shared_worker.ml new file mode 100644 index 0000000000..ce673c9f25 --- /dev/null +++ b/lib/tests-browser/test_shared_worker.ml @@ -0,0 +1,97 @@ +(* Js_of_ocaml tests + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +(* SharedWorker and BroadcastChannel demo (page side). + + Open this page in several tabs: all tabs talk to the same shared worker + (see test_shared_worker_worker.ml), and messages broadcast on the + "jsoo-demo" channel are delivered to every other tab. *) + +open Js_of_ocaml + +let log msg = + let el = Dom_html.document##getElementById (Js.string "log") in + Js.Opt.iter el (fun el -> + let line = Dom_html.document##createElement (Js.string "div") in + line##.textContent := Js.some (Js.string msg); + Dom.appendChild el line) + +let on_click id f = + Js.Opt.iter + (Dom_html.document##getElementById (Js.string id)) + (fun el -> + el##.onclick := + Dom_html.handler (fun _ -> + f (); + Js._false)) + +let is_wasm () = + let search = Js.to_string Dom_html.window##.location##.search in + match Regexp.search (Regexp.regexp "[?&]wasm\\b") search 0 with + | Some _ -> true + | None -> false + +let message_data ev = Js.to_string (Js.Unsafe.coerce ev##.data) + +let setup_shared_worker () = + if not (SharedWorker.is_supported ()) + then log "SharedWorker is not supported in this browser" + else begin + let url = + if is_wasm () + then "test_shared_worker_worker.bc.wasm.js" + else "test_shared_worker_worker.bc.js" + in + let worker = + new%js SharedWorker.sharedWorker_withName (Js.string url) (Js.string "jsoo-demo") + in + worker##.onerror := + Dom.handler (fun _ -> + log "shared worker: error event (see the browser console)"; + Js._true); + worker##.port##.onmessage + := Dom.handler (fun ev -> + log ("shared worker: " ^ message_data ev); + Js._true); + let count = ref 0 in + on_click "send-worker" (fun () -> + incr count; + worker##.port##postMessage (Js.string (Printf.sprintf "ping %d" !count))) + end + +let setup_broadcast_channel () = + if not (BroadcastChannel.is_supported ()) + then log "BroadcastChannel is not supported in this browser" + else begin + let channel = new%js BroadcastChannel.broadcastChannel (Js.string "jsoo-demo") in + channel##.onmessage := + Dom.handler (fun ev -> + log ("broadcast: " ^ message_data ev); + Js._true); + let count = ref 0 in + on_click "send-broadcast" (fun () -> + incr count; + (* Not delivered to this tab: a channel does not receive its own + messages *) + channel##postMessage + (Js.string (Printf.sprintf "hello from another tab (%d)" !count))) + end + +let () = + setup_shared_worker (); + setup_broadcast_channel () diff --git a/lib/tests-browser/test_shared_worker_worker.ml b/lib/tests-browser/test_shared_worker_worker.ml new file mode 100644 index 0000000000..f282dba5ea --- /dev/null +++ b/lib/tests-browser/test_shared_worker_worker.ml @@ -0,0 +1,56 @@ +(* Js_of_ocaml tests + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + *) + +(* SharedWorker and BroadcastChannel demo (worker side). + + A single instance of this worker serves every tab displaying + test_shared_worker.html: it numbers connections, echoes messages with a + shared counter, and announces new connections on the broadcast channel. *) + +open Js_of_ocaml + +let () = + let connections = ref 0 in + let messages = ref 0 in + let broadcast = + if BroadcastChannel.is_supported () + then Some (new%js BroadcastChannel.broadcastChannel (Js.string "jsoo-demo")) + else None + in + SharedWorker.set_onconnect (fun port -> + incr connections; + let id = !connections in + port##postMessage + (Js.string (Printf.sprintf "you are connection #%d to this worker" id)); + (match broadcast with + | Some channel -> + channel##postMessage + (Js.string (Printf.sprintf "connection #%d joined the shared worker" id)) + | None -> ()); + port##.onmessage := + Dom.handler (fun ev -> + incr messages; + let data = Js.to_string (Js.Unsafe.coerce ev##.data) in + port##postMessage + (Js.string + (Printf.sprintf + "connection #%d sent %S (%d messages received from all tabs)" + id + data + !messages)); + Js._true)) diff --git a/lib/tests/test_broadcast_channel.ml b/lib/tests/test_broadcast_channel.ml new file mode 100644 index 0000000000..f8ca9f2f9b --- /dev/null +++ b/lib/tests/test_broadcast_channel.ml @@ -0,0 +1,43 @@ +(* Js_of_ocaml + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +open Js_of_ocaml + +(* [BroadcastChannel] is not available under QuickJS, so the whole suite is + gated with [@when not quickjs]. *) + +let%expect_test ("is_supported" [@when not quickjs]) = + print_endline (if BroadcastChannel.is_supported () then "PASSED" else "FAILED"); + [%expect {| PASSED |}] + +let%expect_test ("channel carries its name" [@when not quickjs]) = + let chan = new%js BroadcastChannel.broadcastChannel (Js.string "jsoo-test") in + let is_broadcast_channel = + Js.instanceof chan (Js.Unsafe.global##._BroadcastChannel : _ Js.constr) + in + print_endline (string_of_bool is_broadcast_channel); + print_endline (Js.to_string chan##.name); + chan##close; + [%expect {| + true + jsoo-test + |}] + +let%expect_test ("channels expose the messaging methods" [@when not quickjs]) = + let chan = new%js BroadcastChannel.broadcastChannel (Js.string "jsoo-test") in + (* [postMessage] must not raise on an open channel. *) + chan##postMessage (Js.string "ping"); + chan##close; + print_endline "PASSED"; + [%expect {| PASSED |}] diff --git a/lib/tests/test_shared_worker.ml b/lib/tests/test_shared_worker.ml new file mode 100644 index 0000000000..5b2068799b --- /dev/null +++ b/lib/tests/test_shared_worker.ml @@ -0,0 +1,47 @@ +(* Js_of_ocaml + * http://www.ocsigen.org/js_of_ocaml/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, with linking exception; + * either version 2.1 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + *) + +open Js_of_ocaml + +(* [SharedWorker] is browser-only (neither Node.js nor QuickJS provide it), so + the suite only exercises the parts that do not depend on the global being + present: building the options record and the capability probes. *) + +let%expect_test "options record can be populated" = + let o = SharedWorker.empty_worker_options () in + o##.name := Js.string "shared"; + o##._type := Js.string "module"; + o##.credentials := Js.string "same-origin"; + (* The fields are write-only; read them back through an unsafe coercion to + check they were stored on the object. *) + print_endline (Js.to_string (Js.Unsafe.coerce o)##.name); + print_endline (Js.to_string (Js.Unsafe.coerce o)##._type); + print_endline (Js.to_string (Js.Unsafe.coerce o)##.credentials); + [%expect {| + shared + module + same-origin + |}] + +let%expect_test "is_supported returns a bool" = + let (_ : bool) = SharedWorker.is_supported () in + print_endline "PASSED"; + [%expect {| PASSED |}] + +let%expect_test "set_onconnect rejects a non-worker scope" = + (* Outside a shared worker there is no [onconnect] global. *) + (match SharedWorker.set_onconnect (fun _ -> ()) with + | () -> print_endline "no exception" + | exception Invalid_argument _ -> print_endline "Invalid_argument"); + [%expect {| Invalid_argument |}] diff --git a/manual/api.mld b/manual/api.mld index 2b7e4dfb1d..3e6732ecd1 100644 --- a/manual/api.mld +++ b/manual/api.mld @@ -26,6 +26,7 @@ Browser APIs: {!modules: Js_of_ocaml.Abort +Js_of_ocaml.BroadcastChannel Js_of_ocaml.CSS Js_of_ocaml.Cache Js_of_ocaml.Clipboard @@ -53,6 +54,7 @@ Js_of_ocaml.Promise Js_of_ocaml.Regexp Js_of_ocaml.ResizeObserver Js_of_ocaml.ServiceWorker +Js_of_ocaml.SharedWorker Js_of_ocaml.Url Js_of_ocaml.WebGL Js_of_ocaml.WebGL2 diff --git a/runtime/wasm/runtime.js b/runtime/wasm/runtime.js index 1e10be52bd..9d58b603da 100644 --- a/runtime/wasm/runtime.js +++ b/runtime/wasm/runtime.js @@ -22,6 +22,18 @@ const isNode = globalThis.process?.versions?.node; + // A shared worker can receive connect events while the wasm module is + // still being instantiated: the worker's event loop runs between the + // script's synchronous evaluation and the end of the asynchronous + // start-up, and nothing listens to these events yet. Capture them and + // dispatch them again once the program has started. + const isSharedWorker = + globalThis.SharedWorkerGlobalScope && + globalThis instanceof globalThis.SharedWorkerGlobalScope; + const pendingConnects = []; + const captureConnect = (event) => pendingConnects.push(event); + if (isSharedWorker) globalThis.addEventListener("connect", captureConnect); + const math = { cos: Math.cos, sin: Math.sin, @@ -944,4 +956,16 @@ ); } await _initialize(); + if (isSharedWorker) { + globalThis.removeEventListener("connect", captureConnect); + for (const event of pendingConnects) { + globalThis.dispatchEvent( + new globalThis.MessageEvent("connect", { + data: event.data, + origin: event.origin, + ports: event.ports, + }), + ); + } + } };