diff --git a/cli/msg.fbs b/cli/msg.fbs index 8f6497afcbcb2e..438fa0cb901bb4 100644 --- a/cli/msg.fbs +++ b/cli/msg.fbs @@ -205,6 +205,8 @@ table FormatErrorRes { table CreateWorker { specifier: string; include_deno_namespace: bool; + has_source_code: bool; + source_code: string; } table CreateWorkerRes { diff --git a/cli/ops.rs b/cli/ops.rs index 410f15a4c1009c..6f98c82bc0d9c0 100644 --- a/cli/ops.rs +++ b/cli/ops.rs @@ -2075,6 +2075,8 @@ fn op_create_worker( // has included namespace (to avoid escalation). let include_deno_namespace = inner.include_deno_namespace() && state.include_deno_namespace; + let has_source_code = inner.has_source_code(); + let source_code = inner.source_code().unwrap(); let parent_state = state.clone(); @@ -2094,29 +2096,34 @@ fn op_create_worker( worker.execute(&deno_main_call).unwrap(); worker.execute("workerMain()").unwrap(); + let exec_cb = move |worker: Worker| { + let mut workers_tl = parent_state.workers.lock().unwrap(); + workers_tl.insert(rid, worker.shared()); + let builder = &mut FlatBufferBuilder::new(); + let msg_inner = + msg::CreateWorkerRes::create(builder, &msg::CreateWorkerResArgs { rid }); + serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(msg_inner.as_union_value()), + inner_type: msg::Any::CreateWorkerRes, + ..Default::default() + }, + ) + }; + + // Has provided source code, execute immediately. + if has_source_code { + worker.execute(&source_code).unwrap(); + return ok_buf(exec_cb(worker)); + } + let module_specifier = ModuleSpecifier::resolve_url_or_path(specifier)?; - let op = - worker - .execute_mod_async(&module_specifier, false) - .and_then(move |()| { - let mut workers_tl = parent_state.workers.lock().unwrap(); - workers_tl.insert(rid, worker.shared()); - let builder = &mut FlatBufferBuilder::new(); - let msg_inner = msg::CreateWorkerRes::create( - builder, - &msg::CreateWorkerResArgs { rid }, - ); - Ok(serialize_response( - cmd_id, - builder, - msg::BaseArgs { - inner: Some(msg_inner.as_union_value()), - inner_type: msg::Any::CreateWorkerRes, - ..Default::default() - }, - )) - }); + let op = worker + .execute_mod_async(&module_specifier, false) + .and_then(move |()| Ok(exec_cb(worker))); let result = op.wait()?; Ok(Op::Sync(result)) diff --git a/js/blob.ts b/js/blob.ts index f2668b642a8df6..94d03c97f53dc4 100644 --- a/js/blob.ts +++ b/js/blob.ts @@ -118,6 +118,10 @@ function processBlobParts( return bytes; } +// A WeakMap holding blob to byte array mapping. +// Ensures it does not impact garbage collection. +export const blobBytesWeakMap = new WeakMap(); + export class DenoBlob implements domTypes.Blob { private readonly [bytesSymbol]: Uint8Array; readonly size: number = 0; @@ -161,6 +165,9 @@ export class DenoBlob implements domTypes.Blob { this[bytesSymbol] = bytes; this.size = bytes.byteLength; this.type = type; + + // Register bytes for internal private use. + blobBytesWeakMap.set(this, bytes); } slice(start?: number, end?: number, contentType?: string): DenoBlob { diff --git a/js/url.ts b/js/url.ts index e8b1ec8f4bc1e4..6520af83489a9c 100644 --- a/js/url.ts +++ b/js/url.ts @@ -1,5 +1,8 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import * as urlSearchParams from "./url_search_params"; +import * as domTypes from "./dom_types"; +import { getRandomValues } from "./get_random_values"; +import { window } from "./window"; interface URLParts { protocol: string; @@ -63,6 +66,20 @@ function parse(url: string): URLParts | undefined { return undefined; } +// Based on https://github.com/kelektiv/node-uuid +// TODO(kevinkassimo): Use deno_std version once possible. +function generateUUID(): string { + return "00000000-0000-4000-8000-000000000000".replace( + /[0]/g, + (): string => + // random integer from 0 to 15 as a hex digit. + (getRandomValues(new Uint8Array(1))[0] % 16).toString(16) + ); +} + +// Keep it outside of URL to avoid any attempts of access. +export const blobURLMap = new Map(); + export class URL { private _parts: URLParts; private _searchParams!: urlSearchParams.URLSearchParams; @@ -273,4 +290,27 @@ export class URL { toJSON(): string { return this.href; } + + // TODO(kevinkassimo): implement MediaSource version in the future. + static createObjectURL(b: domTypes.Blob): string { + const origin = window.location.origin || "http://deno-opaque-origin"; + const key = `blob:${origin}/${generateUUID()}`; + blobURLMap.set(key, b); + return key; + } + + static revokeObjectURL(url: string): void { + let urlObject; + try { + urlObject = new URL(url); + } catch { + throw new TypeError("Provided URL string is not valid"); + } + if (urlObject.protocol !== "blob:") { + return; + } + // Origin match check seems irrelevant for now, unless we implement + // persisten storage for per window.location.origin at some point. + blobURLMap.delete(url); + } } diff --git a/js/workers.ts b/js/workers.ts index f008ccecf5ef01..f061a0126d55e7 100644 --- a/js/workers.ts +++ b/js/workers.ts @@ -6,6 +6,8 @@ import * as flatbuffers from "./flatbuffers"; import { assert, log } from "./util"; import { TextDecoder, TextEncoder } from "./text_encoding"; import { window } from "./window"; +import { blobURLMap } from "./url"; +import { blobBytesWeakMap } from "./blob"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -22,14 +24,19 @@ export function decodeMessage(dataIntArray: Uint8Array): any { function createWorker( specifier: string, - includeDenoNamespace: boolean + includeDenoNamespace: boolean, + hasSourceCode: boolean, + sourceCode: Uint8Array ): number { const builder = flatbuffers.createBuilder(); const specifier_ = builder.createString(specifier); + const sourceCode_ = builder.createString(sourceCode); const inner = msg.CreateWorker.createCreateWorker( builder, specifier_, - includeDenoNamespace + includeDenoNamespace, + hasSourceCode, + sourceCode_ ); const baseRes = sendSync(builder, msg.Any.CreateWorker, inner); assert(baseRes != null); @@ -177,11 +184,33 @@ export class WorkerImpl implements Worker { public onmessageerror?: () => void; constructor(specifier: string, options?: DenoWorkerOptions) { + let hasSourceCode = false; + let sourceCode = new Uint8Array(); + let includeDenoNamespace = true; if (options && options.noDenoNamespace) { includeDenoNamespace = false; } - this.rid = createWorker(specifier, includeDenoNamespace); + // Handle blob URL. + if (specifier.startsWith("blob:")) { + hasSourceCode = true; + const b = blobURLMap.get(specifier); + if (!b) { + throw new Error("No Blob associated with the given URL is found"); + } + const blobBytes = blobBytesWeakMap.get(b!); + if (!blobBytes) { + throw new Error("Invalid Blob"); + } + sourceCode = blobBytes!; + } + + this.rid = createWorker( + specifier, + includeDenoNamespace, + hasSourceCode, + sourceCode + ); this.run(); this.isClosedPromise = hostGetWorkerClosed(this.rid); this.isClosedPromise.then( diff --git a/tests/040_worker_blob.test b/tests/040_worker_blob.test new file mode 100644 index 00000000000000..81fd3182e400b7 --- /dev/null +++ b/tests/040_worker_blob.test @@ -0,0 +1,2 @@ +args: run --reload tests/040_worker_blob.ts +output: tests/040_worker_blob.ts.out diff --git a/tests/040_worker_blob.ts b/tests/040_worker_blob.ts new file mode 100644 index 00000000000000..1ba4528cf9659a --- /dev/null +++ b/tests/040_worker_blob.ts @@ -0,0 +1,6 @@ +const b = new Blob(["console.log('code from Blob'); postMessage('DONE')"]); +const blobURL = URL.createObjectURL(b); +const worker = new Worker(blobURL); +worker.onmessage = (): void => { + Deno.exit(0); +}; diff --git a/tests/040_worker_blob.ts.out b/tests/040_worker_blob.ts.out new file mode 100644 index 00000000000000..f49b8f3d6f5e81 --- /dev/null +++ b/tests/040_worker_blob.ts.out @@ -0,0 +1 @@ +code from Blob