diff --git a/Cargo.lock b/Cargo.lock index 80eb45ffbd..c46c6ac9fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "deno_broadcast_channel" +version = "0.1.0" +dependencies = [ + "deno_core", + "tokio", +] + [[package]] name = "deno_console" version = "0.7.0" @@ -691,6 +699,7 @@ version = "0.15.0" dependencies = [ "atty", "bytes", + "deno_broadcast_channel", "deno_console", "deno_core", "deno_crypto", diff --git a/Cargo.toml b/Cargo.toml index 31e2ee0915..98ea64293c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "serde_v8", "test_plugin", "test_util", + "extensions/broadcast_channel", "extensions/console", "extensions/crypto", "extensions/fetch", diff --git a/cli/build.rs b/cli/build.rs index 116ce8167a..ab2a7d0f7d 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -8,6 +8,7 @@ use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::JsRuntime; use deno_core::RuntimeOptions; +use deno_runtime::deno_broadcast_channel; use deno_runtime::deno_console; use deno_runtime::deno_crypto; use deno_runtime::deno_fetch; @@ -74,6 +75,10 @@ fn create_compiler_snapshot( op_crate_libs.insert("deno.websocket", deno_websocket::get_declaration()); op_crate_libs.insert("deno.webstorage", deno_webstorage::get_declaration()); op_crate_libs.insert("deno.crypto", deno_crypto::get_declaration()); + op_crate_libs.insert( + "deno.broadcast_channel", + deno_broadcast_channel::get_declaration(), + ); // ensure we invalidate the build properly. for (_, path) in op_crate_libs.iter() { @@ -300,6 +305,10 @@ fn main() { "cargo:rustc-env=DENO_CRYPTO_LIB_PATH={}", deno_crypto::get_declaration().display() ); + println!( + "cargo:rustc-env=DENO_BROADCAST_CHANNEL_LIB_PATH={}", + deno_broadcast_channel::get_declaration().display() + ); println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap()); println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap()); diff --git a/cli/main.rs b/cli/main.rs index aa144d1ded..868805e926 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -302,7 +302,7 @@ fn print_cache_info( pub fn get_types(unstable: bool) -> String { let mut types = format!( - "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", crate::tsc::DENO_NS_LIB, crate::tsc::DENO_CONSOLE_LIB, crate::tsc::DENO_URL_LIB, @@ -313,6 +313,7 @@ pub fn get_types(unstable: bool) -> String { crate::tsc::DENO_WEBSOCKET_LIB, crate::tsc::DENO_WEBSTORAGE_LIB, crate::tsc::DENO_CRYPTO_LIB, + crate::tsc::DENO_BROADCAST_CHANNEL_LIB, crate::tsc::SHARED_GLOBALS_LIB, crate::tsc::WINDOW_LIB, ); diff --git a/cli/tsc.rs b/cli/tsc.rs index 6d2e297db8..203bf794b0 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -41,6 +41,8 @@ pub static DENO_WEBSOCKET_LIB: &str = pub static DENO_WEBSTORAGE_LIB: &str = include_str!(env!("DENO_WEBSTORAGE_LIB_PATH")); pub static DENO_CRYPTO_LIB: &str = include_str!(env!("DENO_CRYPTO_LIB_PATH")); +pub static DENO_BROADCAST_CHANNEL_LIB: &str = + include_str!(env!("DENO_BROADCAST_CHANNEL_LIB_PATH")); pub static SHARED_GLOBALS_LIB: &str = include_str!("dts/lib.deno.shared_globals.d.ts"); pub static WINDOW_LIB: &str = include_str!("dts/lib.deno.window.d.ts"); diff --git a/extensions/broadcast_channel/01_broadcast_channel.js b/extensions/broadcast_channel/01_broadcast_channel.js new file mode 100644 index 0000000000..34f8b9e196 --- /dev/null +++ b/extensions/broadcast_channel/01_broadcast_channel.js @@ -0,0 +1,117 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +"use strict"; + +((window) => { + const core = window.Deno.core; + const webidl = window.__bootstrap.webidl; + + const handlerSymbol = Symbol("eventHandlers"); + function makeWrappedHandler(handler) { + function wrappedHandler(...args) { + if (typeof wrappedHandler.handler !== "function") { + return; + } + return wrappedHandler.handler.call(this, ...args); + } + wrappedHandler.handler = handler; + return wrappedHandler; + } + // TODO(lucacasonato) reuse when we can reuse code between web crates + function defineEventHandler(emitter, name) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[handlerSymbol]?.get(name)?.handler; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = this[handlerSymbol]?.get(name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + this[handlerSymbol].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + + const _name = Symbol("[[name]]"); + const _closed = Symbol("[[closed]]"); + const _rid = Symbol("[[rid]]"); + + class BroadcastChannel extends EventTarget { + [_name]; + [_closed] = false; + [_rid]; + + get name() { + return this[_name]; + } + + constructor(name) { + super(); + + window.location; + + const prefix = "Failed to construct 'broadcastChannel'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + this[_name] = webidl.converters["DOMString"](name, { + prefix, + context: "Argument 1", + }); + + this[_rid] = core.opSync("op_broadcast_open", this[_name]); + + this[webidl.brand] = webidl.brand; + + this.#eventLoop(); + } + + postMessage(message) { + webidl.assertBranded(this, BroadcastChannel); + + if (this[_closed]) { + throw new DOMException("Already closed", "InvalidStateError"); + } + + core.opAsync("op_broadcast_send", this[_rid], core.serialize(message)); + } + + close() { + webidl.assertBranded(this, BroadcastChannel); + + this[_closed] = true; + core.close(this[_rid]); + } + + async #eventLoop() { + while (!this[_closed]) { + const message = await core.opAsync( + "op_broadcast_next_event", + this[_rid], + ); + + if (message.length !== 0) { + const event = new MessageEvent("message", { + data: core.deserialize(message), + origin: window.location, + }); + event.target = this; + this.dispatchEvent(event); + } + } + } + } + + defineEventHandler(BroadcastChannel.prototype, "message"); + defineEventHandler(BroadcastChannel.prototype, "messageerror"); + + window.__bootstrap.broadcastChannel = { BroadcastChannel }; +})(this); diff --git a/extensions/broadcast_channel/Cargo.toml b/extensions/broadcast_channel/Cargo.toml new file mode 100644 index 0000000000..72c29f651f --- /dev/null +++ b/extensions/broadcast_channel/Cargo.toml @@ -0,0 +1,18 @@ +# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_broadcast_channel" +version = "0.1.0" +edition = "2018" +description = "Implementation of BroadcastChannel API for Deno" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.88.0", path = "../../core" } +tokio = { version = "1.4.0", features = ["full"] } diff --git a/extensions/broadcast_channel/README.md b/extensions/broadcast_channel/README.md new file mode 100644 index 0000000000..5b5034ef7a --- /dev/null +++ b/extensions/broadcast_channel/README.md @@ -0,0 +1,5 @@ +# deno_broadcast_channel + +This crate implements the BroadcastChannel functions of Deno. + +Spec: https://html.spec.whatwg.org/multipage/web-messaging.html diff --git a/extensions/broadcast_channel/lib.deno_broadcast_channel.d.ts b/extensions/broadcast_channel/lib.deno_broadcast_channel.d.ts new file mode 100644 index 0000000000..c8efef778a --- /dev/null +++ b/extensions/broadcast_channel/lib.deno_broadcast_channel.d.ts @@ -0,0 +1,55 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// deno-lint-ignore-file no-explicit-any + +/// +/// + +interface BroadcastChannelEventMap { + "message": MessageEvent; + "messageerror": MessageEvent; +} + +interface BroadcastChannel extends EventTarget { + /** + * Returns the channel name (as passed to the constructor). + */ + readonly name: string; + onmessage: ((this: BroadcastChannel, ev: MessageEvent) => any) | null; + onmessageerror: ((this: BroadcastChannel, ev: MessageEvent) => any) | null; + /** + * Closes the BroadcastChannel object, opening it up to garbage collection. + */ + close(): void; + /** + * Sends the given message to other BroadcastChannel objects set up for + * this channel. Messages can be structured objects, e.g. nested objects + * and arrays. + */ + postMessage(message: any): void; + addEventListener( + type: K, + listener: (this: BroadcastChannel, ev: BroadcastChannelEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: BroadcastChannel, ev: BroadcastChannelEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +declare var BroadcastChannel: { + prototype: BroadcastChannel; + new (name: string): BroadcastChannel; +}; diff --git a/extensions/broadcast_channel/lib.rs b/extensions/broadcast_channel/lib.rs new file mode 100644 index 0000000000..cee9c3e0c1 --- /dev/null +++ b/extensions/broadcast_channel/lib.rs @@ -0,0 +1,131 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::bad_resource_id; +use deno_core::error::AnyError; +use deno_core::include_js_files; +use deno_core::op_async; +use deno_core::op_sync; +use deno_core::AsyncRefCell; +use deno_core::Extension; +use deno_core::OpState; +use deno_core::RcRef; +use deno_core::Resource; +use deno_core::ResourceId; +use deno_core::ZeroCopyBuf; +use std::borrow::Cow; +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; + +struct BroadcastChannelResource(AsyncRefCell); + +impl Resource for BroadcastChannelResource { + fn name(&self) -> Cow { + "broadcastChannel".into() + } +} + +pub fn op_broadcast_open( + state: &mut OpState, + name: String, + _bufs: Option, +) -> Result { + let path = PathBuf::from("./"); + std::fs::create_dir_all(&path)?; + let file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .read(true) + .open(path.join(format!("broadcast_{}", name)))?; + + let rid = + state + .resource_table + .add(BroadcastChannelResource(AsyncRefCell::new( + tokio::fs::File::from_std(file), + ))); + + Ok(rid) +} + +pub async fn op_broadcast_send( + state: Rc>, + rid: ResourceId, + buf: Option, +) -> Result<(), AnyError> { + let state = state.borrow_mut(); + let resource = state + .resource_table + .get::(rid) + .ok_or_else(bad_resource_id)?; + + let mut file = RcRef::map(&resource, |r| &r.0).borrow_mut().await; + + let buffer_data = buf.unwrap(); + let mut data = vec![]; + data.extend_from_slice(&(buffer_data.len() as u64).to_ne_bytes()); + data.extend_from_slice(&buffer_data); + + file.write_all(&data).await?; + + Ok(()) +} + +pub async fn op_broadcast_next_event( + state: Rc>, + rid: ResourceId, + _bufs: Option, +) -> Result, AnyError> { + let resource = { + let state = state.borrow_mut(); + state + .resource_table + .get::(rid) + .ok_or_else(bad_resource_id)? + }; + + let mut file = RcRef::map(&resource, |r| &r.0).borrow_mut().await; + + let size = match file.read_u64().await { + Ok(s) => s, + Err(e) => { + return match e.kind() { + deno_core::futures::io::ErrorKind::UnexpectedEof => Ok(vec![]), + _ => Err(e.into()), + } + } + }; + let mut data = vec![0u8; size as usize]; + match file.read_exact(&mut data).await { + Ok(s) => s, + Err(e) => { + return match e.kind() { + deno_core::futures::io::ErrorKind::UnexpectedEof => Ok(vec![]), + _ => Err(e.into()), + } + } + }; + + Ok(data) +} + +pub fn init() -> Extension { + Extension::builder() + .js(include_js_files!( + prefix "deno:extensions/broadcast_channel", + "01_broadcast_channel.js", + )) + .ops(vec![ + ("op_broadcast_open", op_sync(op_broadcast_open)), + ("op_broadcast_send", op_async(op_broadcast_send)), + ("op_broadcast_next_event", op_async(op_broadcast_next_event)), + ]) + .build() +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("lib.deno_broadcast_channel.d.ts") +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 5758219d80..c7805b1b16 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -18,6 +18,7 @@ name = "hello_runtime" path = "examples/hello_runtime.rs" [build-dependencies] +deno_broadcast_channel = { path = "../extensions/broadcast_channel", version = "0.1.0" } deno_console = { version = "0.7.0", path = "../extensions/console" } deno_core = { version = "0.88.0", path = "../core" } deno_crypto = { version = "0.21.0", path = "../extensions/crypto" } @@ -36,6 +37,7 @@ winres = "0.1.11" winapi = "0.3.9" [dependencies] +deno_broadcast_channel = { path = "../extensions/broadcast_channel", version = "0.1.0" } deno_console = { version = "0.7.0", path = "../extensions/console" } deno_core = { version = "0.88.0", path = "../core" } deno_crypto = { version = "0.21.0", path = "../extensions/crypto" } diff --git a/runtime/build.rs b/runtime/build.rs index e1cae71954..4fe89af3ec 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -52,6 +52,7 @@ fn create_runtime_snapshot(snapshot_path: &Path, files: Vec) { deno_crypto::init(None), deno_webgpu::init(false), deno_timers::init::(), + deno_broadcast_channel::init(), ]; let js_runtime = JsRuntime::new(RuntimeOptions { diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 796d5178f3..e1c474c282 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -29,6 +29,7 @@ delete Object.prototype.__proto__; const webgpu = window.__bootstrap.webgpu; const webSocket = window.__bootstrap.webSocket; const webStorage = window.__bootstrap.webStorage; + const broadcastChannel = window.__bootstrap.broadcastChannel; const file = window.__bootstrap.file; const formData = window.__bootstrap.formData; const fetch = window.__bootstrap.fetch; @@ -282,6 +283,7 @@ delete Object.prototype.__proto__; URL: util.nonEnumerable(url.URL), URLSearchParams: util.nonEnumerable(url.URLSearchParams), WebSocket: util.nonEnumerable(webSocket.WebSocket), + BroadcastChannel: util.nonEnumerable(broadcastChannel.BroadcastChannel), Worker: util.nonEnumerable(worker.Worker), WritableStream: util.nonEnumerable(streams.WritableStream), WritableStreamDefaultWriter: util.nonEnumerable( diff --git a/runtime/lib.rs b/runtime/lib.rs index d45a727275..3cc73bffff 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +pub use deno_broadcast_channel; pub use deno_console; pub use deno_crypto; pub use deno_fetch; diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index 5b731a0f51..172d24dea2 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -268,6 +268,7 @@ impl WebWorker { options.user_agent.clone(), options.ca_data.clone(), ), + deno_broadcast_channel::init(), deno_crypto::init(options.seed), deno_webgpu::init(options.unstable), deno_timers::init::(), diff --git a/runtime/worker.rs b/runtime/worker.rs index c75f09dc81..b41f0291c5 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -107,6 +107,7 @@ impl MainWorker { ), deno_webstorage::init(options.location_data_dir.clone()), deno_crypto::init(options.seed), + deno_broadcast_channel::init(), deno_webgpu::init(options.unstable), deno_timers::init::(), // Metrics