├── docs ├── README.md ├── stashed-ports.md └── api-idea.md ├── tidyconf.txt ├── tests ├── resources │ ├── empty-worker.js │ ├── accepting-worker.js │ ├── rejecting-worker.js │ ├── echo-worker.js │ ├── testharnessreport.js │ ├── service-worker-loader.html │ ├── testharness.css │ ├── test-helpers.js │ ├── connect.js │ ├── testharness-helpers.js │ ├── service-worker-test-helpers.js │ └── testharness.js ├── connect.html └── connect-cross-origin.html ├── README.md ├── polyfill ├── client-polyfill.js └── service-polyfill.js ├── design-alternatives.md ├── explainer.md ├── LICENSE ├── use-cases.html └── index.html /docs/README.md: -------------------------------------------------------------------------------- 1 | Various documents with ideas for navigator.connect related APIs. 2 | -------------------------------------------------------------------------------- /tidyconf.txt: -------------------------------------------------------------------------------- 1 | char-encoding: utf8 2 | indent: yes 3 | wrap: 100 4 | tidy-mark: no 5 | -------------------------------------------------------------------------------- /tests/resources/empty-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../../polyfill/service-polyfill.js'); 2 | // Do nothing. 3 | -------------------------------------------------------------------------------- /tests/resources/accepting-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../../polyfill/service-polyfill.js'); 2 | 3 | self.addEventListener('crossoriginconnect', function(event) { 4 | event.acceptConnection(true); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/resources/rejecting-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../../polyfill/service-polyfill.js'); 2 | 3 | self.addEventListener('crossoriginconnect', function(event) { 4 | event.acceptConnection(false); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/resources/echo-worker.js: -------------------------------------------------------------------------------- 1 | importScripts('../../polyfill/service-polyfill.js'); 2 | 3 | self.addEventListener('crossoriginconnect', function(event) { 4 | event.acceptConnection(true); 5 | }); 6 | 7 | 8 | self.addEventListener('crossoriginmessage', function(event) { 9 | event.source.postMessage(event.data, event.ports); 10 | }); 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | navigator.connect 2 | ================= 3 | 4 | Making direct connections between cross-origin Javascript contexts. 5 | 6 | Documents in this repository 7 | ---------------------------- 8 | 9 | * [Use Cases and Security Requirements](https://mkruisselbrink.github.io/navigator-connect/use-cases.html) 10 | * [Explainer](explainer.md) 11 | * [Specification](https://mkruisselbrink.github.io/navigator-connect/) 12 | 13 | Communication 14 | ------------- 15 | 16 | * [Github Issue Tracker](https://github.com/mkruisselbrink/navigator-connect/issues) 17 | -------------------------------------------------------------------------------- /tests/resources/testharnessreport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * THIS FILE INTENTIONALLY LEFT BLANK 3 | * 4 | * More specifically, this file is intended for vendors to implement 5 | * code needed to integrate testharness.js tests with their own test systems. 6 | * 7 | * Typically such integration will attach callbacks when each test is 8 | * has run, using add_result_callback(callback(test)), or when the whole test file has 9 | * completed, using add_completion_callback(callback(tests, harness_status)). 10 | * 11 | * For more documentation about the callback functions and the 12 | * parameters they are called with see testharness.js 13 | */ 14 | -------------------------------------------------------------------------------- /tests/connect.html: -------------------------------------------------------------------------------- 1 | 2 | Tests various cases of navigator.connect. 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/connect-cross-origin.html: -------------------------------------------------------------------------------- 1 | 2 | Tests various cases of navigator.connect. 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /polyfill/client-polyfill.js: -------------------------------------------------------------------------------- 1 | // Polyfill that allows navigator.connect connections to service workers that 2 | // include service-polyfill.js. 3 | // navigator.connect returns a promise that resolves to a MessagePort on 4 | // succesfull connection to a service. 5 | if (!navigator.connect) { 6 | navigator.connect = function(url) { 7 | var slashIdx = url.indexOf('/', 10); 8 | var origin = url.substr(0, slashIdx); 9 | var iframe = document.createElement('iframe'); 10 | iframe.style.display = 'none'; 11 | var p = new Promise(function(resolve, reject) { 12 | iframe.onload = function(event) { 13 | var channel = new MessageChannel(); 14 | channel.port1.onmessage = function(event) { 15 | if (event.data.connected) 16 | resolve(event.data.connected); 17 | else 18 | reject({code: 20}); 19 | }; 20 | iframe.contentWindow.postMessage({connect: channel.port2}, '*', [channel.port2]); 21 | }; 22 | }); 23 | iframe.setAttribute('src', url + '?navigator-connect-service'); 24 | document.body.appendChild(iframe); 25 | return p; 26 | }; 27 | } -------------------------------------------------------------------------------- /tests/resources/service-worker-loader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tests/resources/testharness.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; 3 | } 4 | 5 | #log .warning, 6 | #log .warning a { 7 | color: black; 8 | background: yellow; 9 | } 10 | 11 | #log .error, 12 | #log .error a { 13 | color: white; 14 | background: red; 15 | } 16 | 17 | #log pre { 18 | border: 1px solid black; 19 | padding: 1em; 20 | } 21 | 22 | section#summary { 23 | margin-bottom:1em; 24 | } 25 | 26 | table#results { 27 | border-collapse:collapse; 28 | table-layout:fixed; 29 | width:100%; 30 | } 31 | 32 | table#results th:first-child, 33 | table#results td:first-child { 34 | width:4em; 35 | } 36 | 37 | table#results th:last-child, 38 | table#results td:last-child { 39 | width:50%; 40 | } 41 | 42 | table#results.assertions th:last-child, 43 | table#results.assertions td:last-child { 44 | width:35%; 45 | } 46 | 47 | table#results th { 48 | padding:0; 49 | padding-bottom:0.5em; 50 | border-bottom:medium solid black; 51 | } 52 | 53 | table#results td { 54 | padding:1em; 55 | padding-bottom:0.5em; 56 | border-bottom:thin solid black; 57 | } 58 | 59 | tr.pass > td:first-child { 60 | color:green; 61 | } 62 | 63 | tr.fail > td:first-child { 64 | color:red; 65 | } 66 | 67 | tr.timeout > td:first-child { 68 | color:red; 69 | } 70 | 71 | tr.notrun > td:first-child { 72 | color:blue; 73 | } 74 | 75 | .pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { 76 | font-variant:small-caps; 77 | } 78 | 79 | table#results span { 80 | display:block; 81 | } 82 | 83 | table#results span.expected { 84 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 85 | white-space:pre; 86 | } 87 | 88 | table#results span.actual { 89 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 90 | white-space:pre; 91 | } 92 | 93 | -------------------------------------------------------------------------------- /design-alternatives.md: -------------------------------------------------------------------------------- 1 | # Design Alternatives 2 | 3 | This document outlines the various designs that have been proposed for cross-origin communication between ServiceWorkers. It also includes ideas for features/optimizations of the implementation that are of interest, but perhaps not core to the fundamental design. 4 | 5 | This document is meant to be a work in progress that should be updated as new designs and ideas are brought to the table. 6 | 7 | ## Designs for Cross-Origin Communication Between Service Workers 8 | 9 | ### MessagePorts 10 | 11 | - `navigator.connect("https://example.com/services/v1")` can be accepted/rejected by a Service Worker that has registered to handle this URL 12 | - A `MessagePort` is opened between the origins on accept 13 | - Explicit connection attempt to a Service Worker allows potential for browser to pull down the Service Worker if it is not installed 14 | 15 | ### Local look-aside `fetch()` 16 | 17 | - A `local_fetch()` request to `https://example.com/services/v1` is routed to a local Service Worker if one is installed 18 | - The `local_fetch()` does not then go to the network if no Service Worker is installed for the relevant scope 19 | - This is also a somewhat explicit connection attempt to a Service Worker, so the browser could potentially pull down the Service Worker if it is not installed 20 | 21 | ### Universal look-aside `fetch()` 22 | 23 | - Same as the local look-aside `fetch()`, except if no Service Worker is installed for the relevant scope, then the `fetch()`` goes to the network just as it would in today's world 24 | - This new mode for `fetch()` would need to be enabled explicitly, either as an option in the `Request` or as a new kind of scope the cross-origin Service Worker could opt into. 25 | 26 | ## Features & Optimizations 27 | 28 | ### `MessagePort` lifetime management 29 | 30 | [Stashed MessagePorts](https://gist.github.com/mkruisselbrink/536632fcd99d45005064) would allow a Service Worker to transfer a `MessagePort` to or from "external" ownership, in order to allow any `MessagePort` to outlive the JavaScript context of a Service Worker. 31 | 32 | A variant of this idea might allow a Service Worker to keep using the `MessagePort` as an object, but record that it should be stashed when the Service Worker is destroyed. 33 | Then the top-level script could reconstitute its object graph, including `MessagePort`s when it receives a message. 34 | -------------------------------------------------------------------------------- /tests/resources/test-helpers.js: -------------------------------------------------------------------------------- 1 | 2 | function waitForReply(t, port) { 3 | return new Promise(function(resolve) { 4 | var resolved = false; 5 | port.onmessage = t.step_func(function(event) { 6 | assert_false(resolved); 7 | resolved = true; 8 | resolve(event.data); 9 | }); 10 | }); 11 | } 12 | 13 | function sleep(t, ms) { 14 | return new Promise(function(resolve) { 15 | window.setTimeout(resolve, ms); 16 | }); 17 | } 18 | 19 | // Calls |method| in |frame|, storing its output in |output| as well 20 | // as resolving in the output. 21 | function call_method_on_frame(t, frame, method, params, output) { 22 | var channel = new MessageChannel(); 23 | var result = new Promise(function(resolve, reject) { 24 | var got_reply = false; 25 | channel.port1.onmessage = t.step_func(function(event) { 26 | assert_false(got_reply); 27 | got_reply = true; 28 | if (event.data.success) 29 | resolve(event.data.result); 30 | else 31 | reject(event.data.result); 32 | }); 33 | }); 34 | frame.contentWindow.postMessage( 35 | {method: method, params: params, output: output, port: channel.port2}, 36 | '*', [channel.port2]); 37 | return result; 38 | } 39 | 40 | function terminate_service_worker(test, registration) { 41 | return get_newest_worker(registration) 42 | .then(test.step_func(function(worker) { 43 | return internals.terminateServiceWorker(worker); 44 | })); 45 | } 46 | 47 | var HELPERS = window; 48 | 49 | function CrossOriginSWHelper(origin) { 50 | this.origin = origin; 51 | this.frame; 52 | } 53 | 54 | CrossOriginSWHelper.prototype.service_worker_unregister_and_register = function(test, url, scope) { 55 | var self = this; 56 | return with_iframe(self.origin + base_path() + 'resources/service-worker-loader.html') 57 | .then(test.step_func(function(f) { 58 | self.frame = f; 59 | f.style.display = 'none'; 60 | return call_method_on_frame(test, self.frame, 'service_worker_unregister_and_register', [self.origin + base_path() + url, scope], 'registration'); 61 | })).then(test.step_func(function() { 62 | // Use frame as proxy for registration. 63 | return self.frame; 64 | })); 65 | }; 66 | 67 | CrossOriginSWHelper.prototype.wait_for_activated = function(test, registration) { 68 | assert_equals(registration, this.frame); 69 | return call_method_on_frame(test, this.frame, 'wait_for_activated', ['%registration%']); 70 | }; 71 | 72 | CrossOriginSWHelper.prototype.service_worker_unregister = function(test, scope) { 73 | return call_method_on_frame(test, this.frame, 'service_worker_unregister', [scope]); 74 | }; 75 | 76 | CrossOriginSWHelper.prototype.wait_for_update = function(test, registration) { 77 | assert_equals(registration, this.frame); 78 | return call_method_on_frame(test, this.frame, 'wait_for_update', ['%registration%'], 'worker'); 79 | }; 80 | 81 | CrossOriginSWHelper.prototype.terminate_service_worker = function(test, registration) { 82 | return call_method_on_frame(test, this.frame, 'terminate_service_worker', ['%registration%']); 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /docs/stashed-ports.md: -------------------------------------------------------------------------------- 1 | # Stashed MessagePorts 2 | `MessagePort`s aren't too useful in service workers as is, since to be able to use them the javascript code needs to keep a reference to them. This is a proposal for a mechanism to "stash" a `MessagePort` in a service worker so the port can outlive the service workers javascript context. 3 | 4 | ```idl 5 | partial interface ServiceWorkerGlobalScope { 6 | void stashPort(USVString key, MessagePort port); 7 | Promise> getStashedPorts(USVString key); 8 | }; 9 | 10 | interface StashedMessageEvent : MessageEvent { 11 | readonly attribute USVString key; 12 | }; 13 | ``` 14 | 15 | Each Service Worker registration has multiple lists of stashed message ports associated with them, each list with its own key. With the `stashPort` method a service worker can add a new `MessagePort` to the list of ports with a specific key. Once a port has been "stashed" this way, messages sent to it no longer result in `message` events on the port, but instead `message` events (or maybe some new event type) will be sent to the service workers global scope. When a message is from a stashed message port, the source attribute of the `MessageEvent` is set to the `MessagePort`, and additionally a `key` property is present to indicate the key the `MessagePort` was stashed with. 16 | 17 | ## Why is this helpful? 18 | This makes it possible to change the `navigator.connect` proposal to return a MessagePort on both sides of the connection. This is both simpler, and more powerful: now the service side connection can also be transferred, and on top of that if the client side of a navigator.connect channel is a service worker, with this stashed ports thing it is now possible for that port to survive the service worker being shut down. 19 | 20 | ### Updated CrossOriginConnectEvent 21 | ```idl 22 | [Exposed=ServiceWorker] 23 | interface CrossOriginConnectEvent : Event { 24 | readonly attribute DOMString origin; 25 | readonly attribute DOMString targetUrl; 26 | Promise acceptConnection (Promise shouldAccept); 27 | }; 28 | ``` 29 | 30 | ### Sample code 31 | Client side service worker: 32 | ```js 33 | // client-worker.js 34 | navigator.connect('https://example.com/services/push') 35 | .then(function(port) { 36 | port.postMessage({register: 'apikey'}); 37 | self.stashPort('pushService', port); 38 | }); 39 | 40 | self.addEventListener('message', function(e) { 41 | if (e.key === 'pushService') { 42 | // Do something with the message. 43 | } 44 | }); 45 | ``` 46 | 47 | Service side service worker: 48 | ```js 49 | // service-worker.js 50 | self.addEventListener('crossoriginconnect', function(e) { 51 | // Optionally check e.origin 52 | e.acceptConnection(e.targetUrl === 'https://example.com/service/push') 53 | .then(function(port) { 54 | self.stashPort('pushClients', port); 55 | }); 56 | }); 57 | 58 | self.addEventListener('message', function(e) { 59 | if (e.key === 'pushClients') { 60 | // Do something with the data sent 61 | e.source.postMessage('registered'); 62 | } 63 | }); 64 | 65 | self.addEventListener('push', function(e) { 66 | self.getStashedPorts('pushClients') 67 | .then(function(ports) { 68 | for (var i = 0; i < ports.length; ++i) { 69 | ports[i].postMessage('pushmessage'); 70 | } 71 | }); 72 | }); 73 | ``` 74 | -------------------------------------------------------------------------------- /tests/resources/connect.js: -------------------------------------------------------------------------------- 1 | // These tests fail with the polyfill, so don't include them for now. 2 | /* 3 | promise_test(function(test) { 4 | return assert_promise_rejects( 5 | navigator.connect("https://example.com/service/does/not/exists"), 6 | 'AbortError', 7 | 'navigator.connect should fail with an AbortError'); 8 | }, 'Connection fails to a service that doesn exist.', properties); 9 | 10 | 11 | promise_test(function(test) { 12 | var scope = sw_scope + '/empty'; 13 | var sw_url = 'resources/empty-worker.js'; 14 | return assert_promise_rejects( 15 | HELPERS.service_worker_unregister_and_register(test, sw_url, scope) 16 | .then(function(registration) { return HELPERS.wait_for_activated(test, registration); }) 17 | .then(function() { 18 | return navigator.connect(sw_scope + '/service'); 19 | }), 20 | 'AbortError', 21 | 'navigator.connect should fail with an AbortError'); 22 | }, 'Connection fails if service worker doesn\'t handle oncrossoriginconnect.', properties); 23 | */ 24 | 25 | promise_test(function(test) { 26 | var scope = sw_scope + '/rejecting'; 27 | var sw_url = 'resources/rejecting-worker.js'; 28 | return assert_promise_rejects( 29 | HELPERS.service_worker_unregister_and_register(test, sw_url, scope) 30 | .then(function(registration) { return HELPERS.wait_for_activated(test, registration); }) 31 | .then(function() { 32 | return navigator.connect(scope + '/service'); 33 | }), 34 | 'AbortError', 35 | 'navigator.connect should fail with an AbortError'); 36 | }, 'Connection fails if service worker rejects connection event.'); 37 | 38 | promise_test(function(test) { 39 | var scope = sw_scope + '/accepting'; 40 | var sw_url = 'resources/accepting-worker.js'; 41 | return HELPERS.service_worker_unregister_and_register(test, sw_url, scope) 42 | .then(function(registration) { return HELPERS.wait_for_activated(test, registration); }) 43 | .then(function() { 44 | return navigator.connect(scope + '/service'); 45 | }).then(function(port) { 46 | assert_class_string(port, 'MessagePort'); 47 | return HELPERS.service_worker_unregister(test, scope); 48 | }); 49 | }, 'Connection succeeds if service worker accepts connection event.'); 50 | 51 | promise_test(function(test) { 52 | var scope = sw_scope + '/echo'; 53 | var sw_url = 'resources/echo-worker.js'; 54 | return HELPERS.service_worker_unregister_and_register(test, sw_url, scope) 55 | .then(function(registration) { return HELPERS.wait_for_activated(test, registration); }) 56 | .then(function() { 57 | return navigator.connect(scope + '/service'); 58 | }).then(function(port) { 59 | port.postMessage('hello world'); 60 | return waitForReply(test, port); 61 | }).then(function(response) { 62 | assert_equals(response, 'hello world'); 63 | return HELPERS.service_worker_unregister(test, scope); 64 | }); 65 | }, 'Messages can be sent and received.', properties); 66 | 67 | // This test depends on chrome internals to kill the service worker. 68 | /* 69 | promise_test(function(test) { 70 | var scope = sw_scope + '/echo'; 71 | var sw_url = 'resources/echo-worker.js'; 72 | var worker; 73 | var registration; 74 | var port; 75 | return HELPERS.service_worker_unregister_and_register(test, sw_url, scope) 76 | .then(function(reg) { 77 | registration = reg; 78 | return HELPERS.wait_for_activated(test, registration); 79 | }).then(function() { 80 | return navigator.connect(scope + '/service'); 81 | }).then(function(p) { 82 | port = p; 83 | port.postMessage('hello world'); 84 | return waitForReply(test, port); 85 | }).then(function(response) { 86 | assert_equals(response, 'hello world'); 87 | return HELPERS.terminate_service_worker(test, registration); 88 | }).then(function() { 89 | return sleep(test, 100); 90 | }).then(function() { 91 | port.postMessage('hello world again'); 92 | return waitForReply(test, port); 93 | }).then(function(response) { 94 | assert_equals(response, 'hello world again'); 95 | return HELPERS.service_worker_unregister(test, scope); 96 | }); 97 | }, 'Messages can be sent and received even when worker is killed.', properties); 98 | */ 99 | -------------------------------------------------------------------------------- /polyfill/service-polyfill.js: -------------------------------------------------------------------------------- 1 | // Polyfill for the service worker side of navigator.connect. This is not quite 2 | // a perfect polyfill since it doesn't perfectly mimic what the actual API will 3 | // look like. 4 | // 5 | // To use this polyfill, add eventhandlers by calling addEventListener. 6 | // Assigning to oncrossoriginconnnect/oncrossoriginmessage isn't supported. 7 | // 8 | // Furthermore this polyfill might interfere with normal use of fetch and 9 | // message events, although it tries to only handle fetch and message events 10 | // that are specifically related to navigator.connect usage. 11 | // 12 | // The objects passed to crossoriginmessage and crossoriginconnect event 13 | // handlers aren't true events, or even objects of the right type. Additionally 14 | // these 'event' objects don't include all the fields the real events should 15 | // have. 16 | (function(self){ 17 | 18 | if ('oncrossoriginconnect' in self) return; 19 | 20 | var kCrossOriginConnectMessageTag = 'crossOriginConnect'; 21 | var kCrossOriginMessageMessageTag = 'crossOriginMessage'; 22 | var kUrlSuffix = '?navigator-connect-service'; 23 | 24 | var customListeners = {'crossoriginconnect': [], 'crossoriginmessage': []}; 25 | 26 | var addEventListener = self.addEventListener; 27 | self.addEventListener = function(type, listener, useCapture) { 28 | if (type in customListeners) { 29 | customListeners[type].push(listener); 30 | } else { 31 | return addEventListener(type, listener, useCapture); 32 | } 33 | }; 34 | 35 | function dispatchCustomEvent(type, event) { 36 | for (var i = 0; i < customListeners[type].length; ++i) { 37 | customListeners[type][i](event); 38 | } 39 | } 40 | 41 | self.addEventListener('fetch', function(event) { 42 | var targetUrl = event.request.url; 43 | if (targetUrl.indexOf(kUrlSuffix, targetUrl.length - kUrlSuffix.length) === -1) { 44 | // Not a navigator-connect attempt 45 | return; 46 | } 47 | // In the real world this should not reply to all fetches. 48 | event.respondWith( 49 | new Response("", 73 | {headers: {'content-type': 'text/html'}}) 74 | ); 75 | event.stopImmediatePropagation(); 76 | }); 77 | 78 | function CrossOriginServiceWorkerClient(origin, targetUrl, port) { 79 | this.origin = origin; 80 | this.targetUrl = targetUrl; 81 | this.port_ = port; 82 | }; 83 | 84 | CrossOriginServiceWorkerClient.prototype.postMessage = 85 | function(msg, transfer) { 86 | this.port_.postMessage(msg, transfer); 87 | }; 88 | 89 | function CrossOriginConnectEvent(client, port) { 90 | this.client = client; 91 | this.replied_ = false; 92 | this.port_ = port; 93 | }; 94 | 95 | CrossOriginConnectEvent.prototype.acceptConnection = function(accept) { 96 | this.replied_ = true; 97 | this.port_.postMessage({connectResult: accept}); 98 | }; 99 | 100 | function handleCrossOriginConnect(data) { 101 | var targetUrl = data[kCrossOriginConnectMessageTag]; 102 | if (targetUrl.indexOf(kUrlSuffix, targetUrl.length - kUrlSuffix.length) !== -1) { 103 | targetUrl = targetUrl.substr(0, targetUrl.length - kUrlSuffix.length); 104 | } 105 | 106 | var client = 107 | new CrossOriginServiceWorkerClient(data.origin, targetUrl, undefined); 108 | var connectEvent = new CrossOriginConnectEvent(client, data.port); 109 | dispatchCustomEvent('crossoriginconnect', connectEvent); 110 | if (!connectEvent.replied_) { 111 | data.port.postMessage({connectResult: false}); 112 | } 113 | } 114 | 115 | function handleCrossOriginMessage(event) { 116 | var ports = []; 117 | for (var i = 0; i < event.ports; ++i) { 118 | if (event.ports[i] != event.data.port) ports.push(even.ports[i]); 119 | } 120 | 121 | var targetUrl = event.data[kCrossOriginMessageMessageTag]; 122 | if (targetUrl.indexOf(kUrlSuffix, targetUrl.length - kUrlSuffix.length) !== -1) { 123 | targetUrl = targetUrl.substr(0, targetUrl.length - kUrlSuffix.length); 124 | } 125 | 126 | var client = new CrossOriginServiceWorkerClient( 127 | event.data.origin, targetUrl, event.data.port); 128 | var crossOriginMessageEvent = { 129 | data: event.data.data, 130 | ports: ports, 131 | source: client 132 | }; 133 | dispatchCustomEvent('crossoriginmessage', crossOriginMessageEvent); 134 | } 135 | 136 | self.addEventListener('message', function(event) { 137 | // In the real world this should be more careful about what messages to listen to. 138 | if (kCrossOriginConnectMessageTag in event.data) { 139 | handleCrossOriginConnect(event.data); 140 | event.stopImmediatePropagation(); 141 | return; 142 | } 143 | if (kCrossOriginMessageMessageTag in event.data) { 144 | handleCrossOriginMessage(event); 145 | event.stopImmediatePropagation(); 146 | return; 147 | } 148 | }); 149 | 150 | })(self); 151 | -------------------------------------------------------------------------------- /docs/api-idea.md: -------------------------------------------------------------------------------- 1 | ## Client side API 2 | ```webidl 3 | [NoInterfaceObject, Exposed=(Window,Worker)] 4 | interface NavigatorConnect { 5 | Promise connect(USVString url); 6 | }; 7 | 8 | Navigator implements NavigatorConnect; 9 | WorkerNavigator implements NavigatorConnect; 10 | ``` 11 | 12 | ## Service side API 13 | ```webidl 14 | partial interface ServiceWorkerGlobalScope { 15 | attribute EventHandle onconnect; 16 | }; 17 | 18 | [Exposed=ServiceWorker] 19 | interface ConnectEvent { 20 | readonly attribute USVString targetURL; 21 | readonly attribute USVString origin; 22 | void acceptConnection(MessagePort port); 23 | }; 24 | ``` 25 | 26 | * `ConnectEvent` is not a `MessageEvent`, since the event wouldn't have any data/message anyway, and additionally having a `.source` attribute as well as an `acceptConnection` method requires hard to understand/explain behavior. 27 | * `targetURL` is the url the connection was made to, always within the scope of the service worker. 28 | * `origin` is the origin of the client that setup the connection. 29 | * Connection is only accepted if `acceptConnection` is called with a valid `MessagePort`. 30 | 31 | ## Persisted MessagePorts 32 | 33 | ```webidl 34 | partial interface ServiceWorkerGlobalScope { 35 | readonly attribute StashedPortCollection ports; 36 | }; 37 | 38 | [Exposed=ServiceWorker] 39 | interface StashedPortCollection : EventTarget { 40 | // Persists a port, returns the stashed port. The original |port| 41 | // will be neutered by this. 42 | StashedMessagePort add(DOMString name, MessagePort port); 43 | 44 | // Returns all entangled stashed ports for this service worker 45 | // registration matching the name. 46 | Promise> match(DOMString name); 47 | 48 | // Event that is triggered whenever a stashed port receives a message. 49 | // Could just as well be the global onmessage event instead. 50 | // The .source of the MessageEvent is the StashedMessagePort instance. 51 | attribute EventHandler onmessage; 52 | }; 53 | 54 | // Extends MessagePort with a |name| attribute. Besides that ports that are 55 | // an instance of StashedMessagePort won't fire their own omessage event. 56 | // Neutering a StashedMessagePort (for example when transferred) will also 57 | // remove it from StashedPortCollection. 58 | [Exposed=ServiceWorker] 59 | interface StashedMessagePort : MessagePort { 60 | readonly attribute DOMString name; 61 | }; 62 | ``` 63 | 64 | * It's weird to have things that are scoped to a Service Worker Registration exposed via `ServiceWorkerGlobalScope` (generally attributes on the global scope give access to things that are per origin). 65 | * Persisted ports do have to be scoped to a Registration though. A persisted port needs to know what registration to spin up to deliver a message. 66 | * Exposing this on `ServiceWorkerRegistration` has its own sets of problems though. Ordering and other semantics become very complicated if multiple clients and service workers can access/send messages through the same ports at the same time (even if messages are still only delivered to the service worker, and not every context that has access to the port). 67 | * Similarly while persisting a port for a service worker from a client might be nice, that can be done just as easily by just postMessageing he port to the service worker and having the service worker persist the port. That way it's always clear what context "owns" a port. 68 | * On the other hand, even if a per-registration set of persisted ports is only exposed via `ServiceWorkerGlobalScope`, multiple service workers for the same registration would still have access to the same collection of ports. So some of the issues with multiple owners for the same port would still have to be worked out. 69 | * In particular it might become important to figure out which version of a particular service worker registration should get messages; one option would be the active worker, but that could cause problems if a SW wants to persist a connection during install (or activate?). 70 | 71 | # Examples 72 | ```js 73 | // http://acme.com/webapp.js 74 | navigator.connect('https://example.com/services/echo').then( 75 | function(port) { 76 | port.postMessage('hello'); 77 | port.onmessage = function(event) { 78 | // Handle reply from the service. 79 | }; 80 | } 81 | ); 82 | ``` 83 | 84 | ```js 85 | // https://example.com/serviceworker.js 86 | self.addEventListener('connect', function(event) { 87 | // Optionally check event.origin to determine if that origin should be 88 | // allowed access to this service. 89 | if (event.targetUrl === 'https://example.com/services/echo') { 90 | let channel = new MessageChannel; 91 | event.acceptConnection(channel.port2); 92 | let port = channel.port1; 93 | port.addEventListener('message', function(event) { 94 | // Set up a listener 95 | }); 96 | port.postMessage('You are connected!'); 97 | // Port isn't persisted, so when the service worker is killed the 98 | // connection will be closed. 99 | } 100 | }); 101 | ``` 102 | 103 | ```js 104 | // https://example.com/serviceworker.js 105 | self.addEventListener('connect', function(event) { 106 | // Optionally check event.origin to determine if that origin should be 107 | // allowed access to this service. 108 | if (event.targetUrl === 'https://example.com/services/echo') { 109 | let channel = new MessageChannel; 110 | event.acceptConnection(channel.port2); 111 | let port = self.ports.add('echoClient', channel.port1); 112 | port.postMessage('You are connected!'); 113 | } 114 | }); 115 | 116 | self.ports.addEventListener('message', function(event) { 117 | if (event.source.name === 'echoClient') { 118 | event.source.postMessage(event.data); 119 | } 120 | }); 121 | 122 | self.addEventListener('push', function(event) { 123 | event.waitUntil(self.ports.match('pushClient').then( 124 | (ports) => ports.forEach((port) => port.postMessage('received push')))); 125 | }); 126 | ``` 127 | 128 | ```js 129 | // https://acme.com/sw.js 130 | self.addEventListener('install', function(event) { 131 | event.waitUntil(navigator.connect('https://example.com/services/analytics') 132 | .then((port) => self.ports.add('analytics', port))); 133 | }); 134 | 135 | self.addEventListener('fetch', function(event) { 136 | self.ports.match('analytics').then((ports) => ports[0].postMessage('log fetch')); 137 | }); 138 | ``` 139 | -------------------------------------------------------------------------------- /tests/resources/testharness-helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * testharness-helpers contains various useful extensions to testharness.js to 3 | * allow them to be used across multiple tests before they have been 4 | * upstreamed. This file is intended to be usable from both document and worker 5 | * environments, so code should for example not rely on the DOM. 6 | */ 7 | 8 | // A testharness test that simplifies testing with promises. 9 | // 10 | // * The |func| argument should be a reference to a function that optionally 11 | // takes a test object as an argument and returns a Promise (or really any 12 | // thenable object). 13 | // 14 | // * Resolution of the promise completes the test. A rejection causes the test 15 | // to fail. The test harness waits for the promise to resolve. 16 | // 17 | // * Assertions can be made at any point in the promise handlers. Assertions 18 | // only need to be wrapped in test.step_func()s if the promise chain contains 19 | // rejection handlers. 20 | // 21 | // E.g.: 22 | // promise_test(function(t) { 23 | // return method_that_returns_a_promise() 24 | // .then(function(result) { 25 | // assert_equals(result, expected_result, "Should be expected."); 26 | // }); 27 | // }, 'Promise based test'); 28 | function promise_test(func, name, properties) { 29 | properties = properties || {}; 30 | var test = async_test(name, properties); 31 | Promise.resolve(test.step(func, test, test)) 32 | .then(function() { test.done(); }) 33 | .catch(test.step_func(function(value) { 34 | throw value; 35 | })); 36 | } 37 | 38 | // Returns a promise that fulfills after the provided |promise| is fulfilled. 39 | // The |test| succeeds only if |promise| rejects with an exception matching 40 | // |code|. Accepted values for |code| follow those accepted for assert_throws(). 41 | // The optional |description| describes the test being performed. 42 | // 43 | // E.g.: 44 | // assert_promise_rejects( 45 | // new Promise(...), // something that should throw an exception. 46 | // 'NotFoundError', 47 | // 'Should throw NotFoundError.'); 48 | // 49 | // assert_promise_rejects( 50 | // new Promise(...), 51 | // new TypeError(), 52 | // 'Should throw TypeError'); 53 | function assert_promise_rejects(promise, code, description) { 54 | return promise.then( 55 | function() { 56 | throw 'assert_promise_rejects: ' + description + ' Promise did not reject.'; 57 | }, 58 | function(e) { 59 | if (code !== undefined) { 60 | assert_throws(code, function() { throw e; }, description); 61 | } 62 | }); 63 | } 64 | 65 | // Asserts that two objects |actual| and |expected| are weakly equal under the 66 | // following definition: 67 | // 68 | // |a| and |b| are weakly equal if any of the following are true: 69 | // 1. If |a| is not an 'object', and |a| === |b|. 70 | // 2. If |a| is an 'object', and all of the following are true: 71 | // 2.1 |a.p| is weakly equal to |b.p| for all own properties |p| of |a|. 72 | // 2.2 Every own property of |b| is an own property of |a|. 73 | // 74 | // This is a replacement for the the version of assert_object_equals() in 75 | // testharness.js. The latter doesn't handle own properties correctly. I.e. if 76 | // |a.p| is not an own property, it still requires that |b.p| be an own 77 | // property. 78 | // 79 | // Note that |actual| must not contain cyclic references. 80 | self.assert_object_equals = function(actual, expected, description) { 81 | var object_stack = []; 82 | 83 | function _is_equal(actual, expected, prefix) { 84 | if (typeof actual !== 'object') { 85 | assert_equals(actual, expected, prefix); 86 | return; 87 | } 88 | assert_true(typeof expected === 'object', prefix); 89 | assert_equals(object_stack.indexOf(actual), -1, 90 | prefix + ' must not contain cyclic references.'); 91 | 92 | object_stack.push(actual); 93 | 94 | Object.getOwnPropertyNames(expected).forEach(function(property) { 95 | assert_own_property(actual, property, prefix); 96 | _is_equal(actual[property], expected[property], 97 | prefix + '.' + property); 98 | }); 99 | Object.getOwnPropertyNames(actual).forEach(function(property) { 100 | assert_own_property(expected, property, prefix); 101 | }); 102 | 103 | object_stack.pop(); 104 | } 105 | 106 | function _brand(object) { 107 | return Object.prototype.toString.call(object).match(/^\[object (.*)\]$/)[1]; 108 | } 109 | 110 | _is_equal(actual, expected, 111 | (description ? description + ': ' : '') + _brand(actual)); 112 | }; 113 | 114 | // Equivalent to assert_in_array, but uses a weaker equivalence relation 115 | // (assert_object_equals) than '==='. 116 | function assert_object_in_array(actual, expected_array, description) { 117 | assert_true(expected_array.some(function(element) { 118 | try { 119 | assert_object_equals(actual, element); 120 | return true; 121 | } catch (e) { 122 | return false; 123 | } 124 | }), description); 125 | } 126 | 127 | // Assert that the two arrays |actual| and |expected| contain the same set of 128 | // elements as determined by assert_object_equals. The order is not significant. 129 | // 130 | // |expected| is assumed to not contain any duplicates as determined by 131 | // assert_object_equals(). 132 | function assert_array_equivalent(actual, expected, description) { 133 | assert_true(Array.isArray(actual), description); 134 | assert_equals(actual.length, expected.length, description); 135 | expected.forEach(function(expected_element) { 136 | // assert_in_array treats the first argument as being 'actual', and the 137 | // second as being 'expected array'. We are switching them around because 138 | // we want to be resilient against the |actual| array containing 139 | // duplicates. 140 | assert_object_in_array(expected_element, actual, description); 141 | }); 142 | } 143 | 144 | // Asserts that two arrays |actual| and |expected| contain the same set of 145 | // elements as determined by assert_object_equals(). The corresponding elements 146 | // must occupy corresponding indices in their respective arrays. 147 | function assert_array_objects_equals(actual, expected, description) { 148 | assert_true(Array.isArray(actual), description); 149 | assert_equals(actual.length, expected.length, description); 150 | actual.forEach(function(value, index) { 151 | assert_object_equals(value, expected[index], 152 | description + ' : object[' + index + ']'); 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # `navigator.connect()` Explained 2 | 3 | ## What's This All About? 4 | 5 | `navigator.connect()` is like `postMessage` to/from `