diff options
Diffstat (limited to 'extensions')
-rw-r--r-- | extensions/web/02_event.js | 42 | ||||
-rw-r--r-- | extensions/web/03_abort_signal.js | 39 | ||||
-rw-r--r-- | extensions/web/13_message_port.js | 253 | ||||
-rw-r--r-- | extensions/web/Cargo.toml | 1 | ||||
-rw-r--r-- | extensions/web/internal.d.ts | 14 | ||||
-rw-r--r-- | extensions/web/lib.deno_web.d.ts | 89 | ||||
-rw-r--r-- | extensions/web/lib.rs | 22 | ||||
-rw-r--r-- | extensions/web/message_port.rs | 208 | ||||
-rw-r--r-- | extensions/webidl/00_webidl.js | 6 | ||||
-rw-r--r-- | extensions/webidl/internal.d.ts | 15 |
10 files changed, 647 insertions, 42 deletions
diff --git a/extensions/web/02_event.js b/extensions/web/02_event.js index 8ee6acc61..17d5cb54e 100644 --- a/extensions/web/02_event.js +++ b/extensions/web/02_event.js @@ -1129,6 +1129,7 @@ }); this.data = eventInitDict?.data ?? null; + this.ports = eventInitDict?.ports ?? []; this.origin = eventInitDict?.origin ?? ""; this.lastEventId = eventInitDict?.lastEventId ?? ""; } @@ -1196,6 +1197,46 @@ } } + const _eventHandlers = 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(benjamingr) reuse this here and websocket where possible + function defineEventHandler(emitter, name, init) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[_eventHandlers]?.get(name)?.handler; + }, + set(value) { + if (!this[_eventHandlers]) { + this[_eventHandlers] = new Map(); + } + let handlerWrapper = this[_eventHandlers]?.get(name); + if (handlerWrapper) { + console.log("foo"); + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + init?.(this); + } + this[_eventHandlers].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + window.Event = Event; window.EventTarget = EventTarget; window.ErrorEvent = ErrorEvent; @@ -1213,5 +1254,6 @@ window.__bootstrap.event = { setIsTrusted, setTarget, + defineEventHandler, }; })(this); diff --git a/extensions/web/03_abort_signal.js b/extensions/web/03_abort_signal.js index 6551380da..54a485dab 100644 --- a/extensions/web/03_abort_signal.js +++ b/extensions/web/03_abort_signal.js @@ -3,7 +3,7 @@ ((window) => { const webidl = window.__bootstrap.webidl; - const { setIsTrusted } = window.__bootstrap.event; + const { setIsTrusted, defineEventHandler } = window.__bootstrap.event; const add = Symbol("add"); const signalAbort = Symbol("signalAbort"); @@ -81,43 +81,6 @@ webidl.configurePrototype(AbortController); - 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(benjamingr) reuse this here and websocket where possible - 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, - }); - } - webidl.converters["AbortSignal"] = webidl.createInterfaceConverter( "AbortSignal", AbortSignal, diff --git a/extensions/web/13_message_port.js b/extensions/web/13_message_port.js new file mode 100644 index 000000000..50d3f5d04 --- /dev/null +++ b/extensions/web/13_message_port.js @@ -0,0 +1,253 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../webidl/internal.d.ts" /> +/// <reference path="./internal.d.ts" /> +/// <reference path="./lib.deno_web.d.ts" /> + +"use strict"; + +((window) => { + const core = window.Deno.core; + const webidl = window.__bootstrap.webidl; + const { setEventTargetData } = window.__bootstrap.eventTarget; + const { defineEventHandler } = window.__bootstrap.event; + + class MessageChannel { + /** @type {MessagePort} */ + #port1; + /** @type {MessagePort} */ + #port2; + + constructor() { + this[webidl.brand] = webidl.brand; + const [port1Id, port2Id] = opCreateEntangledMessagePort(); + const port1 = createMessagePort(port1Id); + const port2 = createMessagePort(port2Id); + this.#port1 = port1; + this.#port2 = port2; + } + + get port1() { + webidl.assertBranded(this, MessageChannel); + return this.#port1; + } + + get port2() { + webidl.assertBranded(this, MessageChannel); + return this.#port2; + } + + [Symbol.for("Deno.inspect")](inspect) { + return `MessageChannel ${ + inspect({ port1: this.port1, port2: this.port2 }) + }`; + } + + get [Symbol.toStringTag]() { + return "MessageChannel"; + } + } + + webidl.configurePrototype(MessageChannel); + + const _id = Symbol("id"); + const _enabled = Symbol("enabled"); + + /** + * @param {number} id + * @returns {MessagePort} + */ + function createMessagePort(id) { + const port = webidl.createBranded(MessagePort); + setEventTargetData(port); + port[_id] = id; + return port; + } + + class MessagePort extends EventTarget { + /** @type {number | null} */ + [_id] = null; + /** @type {boolean} */ + [_enabled] = false; + + constructor() { + super(); + webidl.illegalConstructor(); + } + + /** + * @param {any} message + * @param {object[] | PostMessageOptions} transferOrOptions + */ + postMessage(message, transferOrOptions = {}) { + webidl.assertBranded(this, MessagePort); + const prefix = "Failed to execute 'postMessage' on 'MessagePort'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + message = webidl.converters.any(message); + let options; + if ( + webidl.type(transferOrOptions) === "Object" && + transferOrOptions !== undefined && + transferOrOptions[Symbol.iterator] !== undefined + ) { + const transfer = webidl.converters["sequence<object>"]( + transferOrOptions, + { prefix, context: "Argument 2" }, + ); + options = { transfer }; + } else { + options = webidl.converters.PostMessageOptions(transferOrOptions, { + prefix, + context: "Argument 2", + }); + } + const { transfer } = options; + if (transfer.includes(this)) { + throw new DOMException("Can not tranfer self", "DataCloneError"); + } + const data = serializeJsMessageData(message, transfer); + if (this[_id] === null) return; + core.opSync("op_message_port_post_message", this[_id], data); + } + + start() { + webidl.assertBranded(this, MessagePort); + if (this[_enabled]) return; + (async () => { + this[_enabled] = true; + while (true) { + if (this[_id] === null) break; + const data = await core.opAsync( + "op_message_port_recv_message", + this[_id], + ); + if (data === null) break; + let message, transfer; + try { + const v = deserializeJsMessageData(data); + message = v[0]; + transfer = v[1]; + } catch (err) { + const event = new MessageEvent("messageerror", { data: err }); + this.dispatchEvent(event); + return; + } + const event = new MessageEvent("message", { + data: message, + ports: transfer, + }); + this.dispatchEvent(event); + } + this[_enabled] = false; + })(); + } + + close() { + webidl.assertBranded(this, MessagePort); + if (this[_id] !== null) { + core.close(this[_id]); + this[_id] = null; + } + } + } + + defineEventHandler(MessagePort.prototype, "message", function (self) { + self.start(); + }); + defineEventHandler(MessagePort.prototype, "messageerror"); + + webidl.configurePrototype(MessagePort); + + /** + * @returns {[number, number]} + */ + function opCreateEntangledMessagePort() { + return core.opSync("op_message_port_create_entangled"); + } + + /** + * @param {globalThis.__bootstrap.messagePort.MessageData} messageData + * @returns {[any, object[]]} + */ + function deserializeJsMessageData(messageData) { + /** @type {object[]} */ + const transferables = []; + + for (const transferable of messageData.transferables) { + switch (transferable.kind) { + case "messagePort": { + const port = createMessagePort(transferable.data); + transferables.push(port); + break; + } + default: + throw new TypeError("Unreachable"); + } + } + + const data = core.deserialize(messageData.data); + + return [data, transferables]; + } + + /** + * @param {any} data + * @param {object[]} tranferables + * @returns {globalThis.__bootstrap.messagePort.MessageData} + */ + function serializeJsMessageData(data, tranferables) { + let serializedData; + try { + serializedData = core.serialize(data); + } catch (err) { + throw new DOMException(err.message, "DataCloneError"); + } + + /** @type {globalThis.__bootstrap.messagePort.Transferable[]} */ + const serializedTransferables = []; + + for (const transferable of tranferables) { + if (transferable instanceof MessagePort) { + webidl.assertBranded(transferable, MessagePort); + const id = transferable[_id]; + if (id === null) { + throw new DOMException( + "Can not transfer disentangled message port", + "DataCloneError", + ); + } + transferable[_id] = null; + serializedTransferables.push({ kind: "messagePort", data: id }); + } else { + throw new DOMException("Value not transferable", "DataCloneError"); + } + } + + return { + data: serializedData, + transferables: serializedTransferables, + }; + } + + webidl.converters.PostMessageOptions = webidl.createDictionaryConverter( + "PostMessageOptions", + [ + { + key: "transfer", + converter: webidl.converters["sequence<object>"], + get defaultValue() { + return []; + }, + }, + ], + ); + + window.__bootstrap.messagePort = { + MessageChannel, + MessagePort, + deserializeJsMessageData, + serializeJsMessageData, + }; +})(globalThis); diff --git a/extensions/web/Cargo.toml b/extensions/web/Cargo.toml index 5da9092f3..33b75143b 100644 --- a/extensions/web/Cargo.toml +++ b/extensions/web/Cargo.toml @@ -18,6 +18,7 @@ base64 = "0.13.0" deno_core = { version = "0.91.0", path = "../../core" } encoding_rs = "0.8.28" serde = "1.0" +tokio = "1.7" uuid = { version = "0.8.2", features = ["v4"] } [dev-dependencies] diff --git a/extensions/web/internal.d.ts b/extensions/web/internal.d.ts index 8ab101077..06976b28b 100644 --- a/extensions/web/internal.d.ts +++ b/extensions/web/internal.d.ts @@ -4,9 +4,6 @@ /// <reference lib="esnext" /> declare namespace globalThis { - declare var TextEncoder: typeof TextEncoder; - declare var TextDecoder: typeof TextDecoder; - declare namespace __bootstrap { declare var infra: { collectSequenceOfCodepoints( @@ -85,5 +82,16 @@ declare namespace globalThis { ReadableStream: typeof ReadableStream; isReadableStreamDisturbed(stream: ReadableStream): boolean; }; + + declare namespace messagePort { + declare type Transferable = { + kind: "messagePort"; + data: number; + }; + declare interface MessageData { + data: Uint8Array; + transferables: Transferable[]; + } + } } } diff --git a/extensions/web/lib.deno_web.d.ts b/extensions/web/lib.deno_web.d.ts index 888fe9de9..6c79f61bd 100644 --- a/extensions/web/lib.deno_web.d.ts +++ b/extensions/web/lib.deno_web.d.ts @@ -648,3 +648,92 @@ interface TransformStreamDefaultControllerTransformCallback<I, O> { controller: TransformStreamDefaultController<O>, ): void | PromiseLike<void>; } + +interface MessageEventInit<T = any> extends EventInit { + data?: T; + origin?: string; + lastEventId?: string; +} + +declare class MessageEvent<T = any> extends Event { + /** + * Returns the data of the message. + */ + readonly data: T; + /** + * Returns the last event ID string, for server-sent events. + */ + readonly lastEventId: string; + /** + * Returns transfered ports. + */ + readonly ports: ReadonlyArray<MessagePort>; + constructor(type: string, eventInitDict?: MessageEventInit); +} + +type Transferable = ArrayBuffer | MessagePort; + +interface PostMessageOptions { + transfer?: Transferable[]; +} + +/** The MessageChannel interface of the Channel Messaging API allows us to + * create a new message channel and send data through it via its two MessagePort + * properties. */ +declare class MessageChannel { + constructor(); + readonly port1: MessagePort; + readonly port2: MessagePort; +} + +interface MessagePortEventMap { + "message": MessageEvent; + "messageerror": MessageEvent; +} + +/** The MessagePort interface of the Channel Messaging API represents one of the + * two ports of a MessageChannel, allowing messages to be sent from one port and + * listening out for them arriving at the other. */ +declare class MessagePort extends EventTarget { + onmessage: ((this: MessagePort, ev: MessageEvent) => any) | null; + onmessageerror: ((this: MessagePort, ev: MessageEvent) => any) | null; + /** + * Disconnects the port, so that it is no longer active. + */ + close(): void; + /** + * Posts a message through the channel. Objects listed in transfer are + * transferred, not just cloned, meaning that they are no longer usable on the + * sending side. + * + * Throws a "DataCloneError" DOMException if transfer contains duplicate + * objects or port, or if message could not be cloned. + */ + postMessage(message: any, transfer: Transferable[]): void; + postMessage(message: any, options?: PostMessageOptions): void; + /** + * Begins dispatching messages received on the port. This is implictly called + * when assiging a value to `this.onmessage`. + */ + start(): void; + addEventListener<K extends keyof MessagePortEventMap>( + type: K, + listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener<K extends keyof MessagePortEventMap>( + type: K, + listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} diff --git a/extensions/web/lib.rs b/extensions/web/lib.rs index 9f7836620..d74bb619d 100644 --- a/extensions/web/lib.rs +++ b/extensions/web/lib.rs @@ -1,11 +1,16 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +mod message_port; + +pub use crate::message_port::JsMessageData; + use deno_core::error::bad_resource_id; use deno_core::error::null_opbuf; use deno_core::error::range_error; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::include_js_files; +use deno_core::op_async; use deno_core::op_sync; use deno_core::url::Url; use deno_core::Extension; @@ -30,6 +35,10 @@ use std::sync::Mutex; use std::usize; use uuid::Uuid; +use crate::message_port::op_message_port_create_entangled; +use crate::message_port::op_message_port_post_message; +use crate::message_port::op_message_port_recv_message; + /// Load and execute the javascript code. pub fn init( blob_url_store: BlobUrlStore, @@ -52,6 +61,7 @@ pub fn init( "10_filereader.js", "11_blob_url.js", "12_location.js", + "13_message_port.js", )) .ops(vec![ ("op_base64_decode", op_sync(op_base64_decode)), @@ -71,6 +81,18 @@ pub fn init( "op_file_revoke_object_url", op_sync(op_file_revoke_object_url), ), + ( + "op_message_port_create_entangled", + op_sync(op_message_port_create_entangled), + ), + ( + "op_message_port_post_message", + op_sync(op_message_port_post_message), + ), + ( + "op_message_port_recv_message", + op_async(op_message_port_recv_message), + ), ]) .state(move |state| { state.put(blob_url_store.clone()); diff --git a/extensions/web/message_port.rs b/extensions/web/message_port.rs new file mode 100644 index 000000000..d10b455d5 --- /dev/null +++ b/extensions/web/message_port.rs @@ -0,0 +1,208 @@ +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +use deno_core::error::bad_resource_id; +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::ZeroCopyBuf; +use deno_core::{CancelFuture, Resource}; +use deno_core::{CancelHandle, OpState}; +use deno_core::{RcRef, ResourceId}; +use serde::Deserialize; +use serde::Serialize; +use tokio::sync::mpsc::unbounded_channel; +use tokio::sync::mpsc::UnboundedReceiver; +use tokio::sync::mpsc::UnboundedSender; + +enum Transferable { + MessagePort(MessagePort), +} + +type MessagePortMessage = (Vec<u8>, Vec<Transferable>); + +pub struct MessagePort { + rx: RefCell<UnboundedReceiver<MessagePortMessage>>, + tx: UnboundedSender<MessagePortMessage>, +} + +impl MessagePort { + pub fn send( + &self, + state: &mut OpState, + data: JsMessageData, + ) -> Result<(), AnyError> { + let transferables = + deserialize_js_transferables(state, data.transferables)?; + + // Swallow the failed to send error. It means the channel was disentangled, + // but not cleaned up. + self.tx.send((data.data.to_vec(), transferables)).ok(); + + Ok(()) + } + + pub async fn recv( + &self, + state: Rc<RefCell<OpState>>, + ) -> Result<Option<JsMessageData>, AnyError> { + let mut rx = self + .rx + .try_borrow_mut() + .map_err(|_| type_error("Port receiver is already borrowed"))?; + if let Some((data, transferables)) = rx.recv().await { + let js_transferables = + serialize_transferables(&mut state.borrow_mut(), transferables); + return Ok(Some(JsMessageData { + data: ZeroCopyBuf::from(data), + transferables: js_transferables, + })); + } + Ok(None) + } +} + +pub fn create_entangled_message_port() -> (MessagePort, MessagePort) { + let (port1_tx, port2_rx) = unbounded_channel::<MessagePortMessage>(); + let (port2_tx, port1_rx) = unbounded_channel::<MessagePortMessage>(); + + let port1 = MessagePort { + rx: RefCell::new(port1_rx), + tx: port1_tx, + }; + + let port2 = MessagePort { + rx: RefCell::new(port2_rx), + tx: port2_tx, + }; + + (port1, port2) +} + +pub struct MessagePortResource { + port: MessagePort, + cancel: CancelHandle, +} + +impl Resource for MessagePortResource { + fn name(&self) -> Cow<str> { + "messagePort".into() + } + + fn close(self: Rc<Self>) { + self.cancel.cancel(); + } +} + +pub fn op_message_port_create_entangled( + state: &mut OpState, + _: (), + _: (), +) -> Result<(ResourceId, ResourceId), AnyError> { + let (port1, port2) = create_entangled_message_port(); + + let port1_id = state.resource_table.add(MessagePortResource { + port: port1, + cancel: CancelHandle::new(), + }); + + let port2_id = state.resource_table.add(MessagePortResource { + port: port2, + cancel: CancelHandle::new(), + }); + + Ok((port1_id, port2_id)) +} + +#[derive(Deserialize, Serialize)] +#[serde(tag = "kind", content = "data", rename_all = "camelCase")] +pub enum JsTransferable { + #[serde(rename_all = "camelCase")] + MessagePort(ResourceId), +} + +fn deserialize_js_transferables( + state: &mut OpState, + js_transferables: Vec<JsTransferable>, +) -> Result<Vec<Transferable>, AnyError> { + let mut transferables = Vec::with_capacity(js_transferables.len()); + for js_transferable in js_transferables { + match js_transferable { + JsTransferable::MessagePort(id) => { + let resource = state + .resource_table + .take::<MessagePortResource>(id) + .ok_or_else(|| type_error("Invalid message port transfer"))?; + resource.cancel.cancel(); + let resource = Rc::try_unwrap(resource) + .map_err(|_| type_error("Message port is not ready for transfer"))?; + transferables.push(Transferable::MessagePort(resource.port)); + } + } + } + Ok(transferables) +} + +fn serialize_transferables( + state: &mut OpState, + transferables: Vec<Transferable>, +) -> Vec<JsTransferable> { + let mut js_transferables = Vec::with_capacity(transferables.len()); + for transferable in transferables { + match transferable { + Transferable::MessagePort(port) => { + let rid = state.resource_table.add(MessagePortResource { + port, + cancel: CancelHandle::new(), + }); + js_transferables.push(JsTransferable::MessagePort(rid)); + } + } + } + js_transferables +} + +#[derive(Deserialize, Serialize)] +pub struct JsMessageData { + data: ZeroCopyBuf, + transferables: Vec<JsTransferable>, +} + +pub fn op_message_port_post_message( + state: &mut OpState, + rid: ResourceId, + data: JsMessageData, +) -> Result<(), AnyError> { + for js_transferable in &data.transferables { + match js_transferable { + JsTransferable::MessagePort(id) => { + if *id == rid { + return Err(type_error("Can not transfer self message port")); + } + } + } + } + + let resource = state + .resource_table + .get::<MessagePortResource>(rid) + .ok_or_else(bad_resource_id)?; + + resource.port.send(state, data) +} + +pub async fn op_message_port_recv_message( + state: Rc<RefCell<OpState>>, + rid: ResourceId, + _: (), +) -> Result<Option<JsMessageData>, AnyError> { + let resource = { + let state = state.borrow(); + match state.resource_table.get::<MessagePortResource>(rid) { + Some(resource) => resource, + None => return Ok(None), + } + }; + let cancel = RcRef::map(resource.clone(), |r| &r.cancel); + resource.port.recv(state.clone()).or_cancel(cancel).await? +} diff --git a/extensions/webidl/00_webidl.js b/extensions/webidl/00_webidl.js index 87e9eccb7..3e6dc95eb 100644 --- a/extensions/webidl/00_webidl.js +++ b/extensions/webidl/00_webidl.js @@ -564,7 +564,10 @@ converters.USVString, ); converters["sequence<double>"] = createSequenceConverter( - converters["double"], + converters.double, + ); + converters["sequence<object>"] = createSequenceConverter( + converters.object, ); converters["Promise<undefined>"] = createPromiseConverter(() => undefined); @@ -630,6 +633,7 @@ get() { return member.defaultValue; }, + enumerable: true, }); } } diff --git a/extensions/webidl/internal.d.ts b/extensions/webidl/internal.d.ts index 4d0f1ad45..9d151a6d1 100644 --- a/extensions/webidl/internal.d.ts +++ b/extensions/webidl/internal.d.ts @@ -321,6 +321,21 @@ declare namespace globalThis { * Configure prototype properties enumerability / writability / configurability. */ declare function configurePrototype(prototype: any); + + /** + * Get the WebIDL / ES type of a value. + */ + declare function type( + v: any, + ): + | "Null" + | "Undefined" + | "Boolean" + | "Number" + | "String" + | "Symbol" + | "BigInt" + | "Object"; } } } |