├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── modules.xml ├── remote-web-streams.iml └── vcs.xml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs └── examples │ ├── index.html │ ├── page-array.html │ ├── page-stream.html │ ├── process.js │ ├── resources │ └── jank-meter.css │ ├── test │ ├── index.html │ └── worker.js │ ├── utils.js │ ├── worker-array.html │ ├── worker-array.js │ ├── worker-stream-transferable.html │ ├── worker-stream-transferable.js │ ├── worker-stream.html │ └── worker-stream.js ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── src ├── index.ts ├── protocol.ts ├── readable.ts ├── remote.ts ├── transfer.ts └── writable.ts ├── test ├── mocks │ ├── EventTarget.ts │ ├── MessageChannel.ts │ └── dom.ts ├── promise-utils.ts ├── remote.spec.ts └── tsconfig.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.local.xml 15 | .idea/**/sqlDataSources.xml 16 | .idea/**/dynamic.xml 17 | .idea/**/uiDesigner.xml 18 | 19 | # Gradle: 20 | .idea/**/gradle.xml 21 | .idea/**/libraries 22 | 23 | # CMake 24 | cmake-build-debug/ 25 | cmake-build-release/ 26 | 27 | # Mongo Explorer plugin: 28 | .idea/**/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.iws 32 | 33 | ## Plugin-specific files: 34 | 35 | # IntelliJ 36 | out/ 37 | 38 | # mpeltonen/sbt-idea plugin 39 | .idea_modules/ 40 | 41 | # JIRA plugin 42 | atlassian-ide-plugin.xml 43 | 44 | # Cursive Clojure plugin 45 | .idea/replstate.xml 46 | 47 | # Crashlytics plugin (for Android Studio and IntelliJ) 48 | com_crashlytics_export_strings.xml 49 | crashlytics.properties 50 | crashlytics-build.properties 51 | fabric.properties 52 | 53 | ### Node template 54 | # Logs 55 | logs 56 | *.log 57 | npm-debug.log* 58 | yarn-debug.log* 59 | yarn-error.log* 60 | 61 | # Runtime data 62 | pids 63 | *.pid 64 | *.seed 65 | *.pid.lock 66 | 67 | # Directory for instrumented libs generated by jscoverage/JSCover 68 | lib-cov 69 | 70 | # Coverage directory used by tools like istanbul 71 | coverage 72 | 73 | # nyc test coverage 74 | .nyc_output 75 | 76 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 77 | .grunt 78 | 79 | # Bower dependency directory (https://bower.io/) 80 | bower_components 81 | 82 | # node-waf configuration 83 | .lock-wscript 84 | 85 | # Compiled binary addons (https://nodejs.org/api/addons.html) 86 | build/Release 87 | 88 | # Dependency directories 89 | node_modules/ 90 | jspm_packages/ 91 | 92 | # Typescript v1 declaration files 93 | typings/ 94 | 95 | # Optional npm cache directory 96 | .npm 97 | 98 | # Optional eslint cache 99 | .eslintcache 100 | 101 | # Optional REPL history 102 | .node_repl_history 103 | 104 | # Output of 'npm pack' 105 | *.tgz 106 | 107 | # Yarn Integrity file 108 | .yarn-integrity 109 | 110 | # dotenv environment variables file 111 | .env 112 | 113 | # next.js build output 114 | .next 115 | 116 | ### Project 117 | dist/ 118 | tmp/ 119 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/remote-web-streams.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > - 💥 Breaking Change 5 | > - 👓 Spec Compliance 6 | > - 🚀 New Feature 7 | > - 🐛 Bug Fix 8 | > - 👎 Deprecation 9 | > - 📝 Documentation 10 | > - 🏠 Internal 11 | > - 💅 Polish 12 | 13 | ## v0.2.0 (2023-06-27) 14 | 15 | * 💥 This package is now ESM-only, and uses ES2020 syntax. ([#25](https://github.com/MattiasBuelens/remote-web-streams/issues/25), [#26](https://github.com/MattiasBuelens/remote-web-streams/pull/26)) 16 | 17 | ## v0.1.0 (2018-07-15) 18 | 19 | * 🚀 Initial release. 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mattias Buelens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # remote-web-streams 2 | [Web streams][streams-spec] that work across web workers and ``s. 3 | 4 | ## Problem 5 | Suppose you want to process some data that you've downloaded somewhere. The processing is quite CPU-intensive, 6 | so you want to do it inside a worker. No problem, the web has you covered with `postMessage`! 7 | 8 | ```js 9 | // main.js 10 | (async () => { 11 | const response = await fetch('./some-data.txt'); 12 | const data = await response.text(); 13 | const worker = new Worker('./worker.js'); 14 | worker.onmessage = (event) => { 15 | const output = event.data; 16 | const results = document.getElementById('results'); 17 | results.appendChild(document.createTextNode(output)); // tadaa! 18 | }; 19 | worker.postMessage(data); 20 | })(); 21 | 22 | // worker.js 23 | self.onmessage = (event) => { 24 | const input = event.data; 25 | const output = process(input); // do the actual work 26 | self.postMessage(output); 27 | } 28 | ``` 29 | 30 | All is good: your processing does not block the main thread, so your web page remains responsive. However, it takes 31 | quite a long time before the results show up: first *all* of the data needs to be downloaded, then *all* that data 32 | needs to be processed, and *finally* everything is shown on the page. Wouldn't it be nice if we could already show 33 | something as soon as *some* of the data has been downloaded and processed? 34 | 35 | Normally, you'd tackle this with by reading the input as a stream, piping it through one or more transform streams 36 | and finally displaying the results as they come in. 37 | 38 | ```js 39 | // main.js 40 | (async () => { 41 | const response = await fetch('./some-data.txt'); 42 | await response.body 43 | .pipeThrough(new TransformStream({ 44 | transform(chunk, controller) { 45 | controller.enqueue(process(chunk)); // do the actual work 46 | } 47 | })) 48 | .pipeTo(new WritableStream({ 49 | write(chunk) { 50 | const results = document.getElementById('results'); 51 | results.appendChild(document.createTextNode(chunk)); // tadaa! 52 | } 53 | })); 54 | })(); 55 | ``` 56 | 57 | Now you can see the first results as they come in, but your processing is blocking the main thread again! 58 | Can we get the best of both worlds: **process data as it comes in, but off the main thread**? 59 | 60 | ## Solution 61 | Enter: `remote-web-streams`. With this libray, you can create pairs of readable and writable streams 62 | where you can write chunks to a writable stream inside one context, and read those chunks from a readable stream 63 | **inside a different context**. 64 | Functionally, such a pair behaves just like an [identity transform stream][identity-transform-stream], and you can 65 | use and compose them just like any other stream. 66 | 67 | ## Basic setup 68 | 69 | ### RemoteReadableStream 70 | The basic steps for setting up a pair of linked streams are: 71 | 1. Construct a `RemoteReadableStream`. This returns two objects: 72 | * a `MessagePort` which must be used to construct the linked `WritableStream` inside the other context 73 | * a `ReadableStream` which will read chunks written by the linked `WritableStream` 74 | ```js 75 | // main.js 76 | import { RemoteReadableStream } from 'remote-web-streams'; 77 | const { readable, writablePort } = new RemoteReadableStream(); 78 | ``` 79 | 2. Transfer the `writablePort` to the other context, and instantiate the linked `WritableStream` in that context 80 | using `fromWritablePort`. 81 | ```js 82 | // main.js 83 | const worker = new Worker('./worker.js', { type: 'module' }); 84 | worker.postMessage({ writablePort }, [writablePort]); 85 | 86 | // worker.js 87 | import { fromWritablePort } from 'remote-web-streams'; 88 | self.onmessage = (event) => { 89 | const { writablePort } = event.data; 90 | const writable = RemoteWebStreams.fromWritablePort(writablePort); 91 | } 92 | ``` 93 | 3. Use the streams as usual! Whenever you write something to the `writable` inside one context, 94 | the `readable` in the other context will receive it. 95 | ```js 96 | // worker.js 97 | const writer = writable.getWriter(); 98 | writer.write('hello'); 99 | writer.write('world'); 100 | writer.close(); 101 | 102 | // main.js 103 | (async () => { 104 | const reader = readable.getReader(); 105 | console.log(await reader.read()); // { done: false, value: 'hello' } 106 | console.log(await reader.read()); // { done: false, value: 'world' } 107 | console.log(await reader.read()); // { done: true, value: undefined } 108 | })(); 109 | ``` 110 | 111 | ### RemoteWritableStream 112 | You can also create a `RemoteWritableStream`. 113 | This is the complement to `RemoteReadableStream`: 114 | * The constructor (in the original context) returns a `WritableStream` (instead of a readable one). 115 | * You transfer the `readablePort` to the other context, 116 | and instantiate the linked `ReadableStream` with `fromReadablePort` inside that context. 117 | ```js 118 | // main.js 119 | import { RemoteWritableStream } from 'remote-web-streams'; 120 | worker.postMessage({ readablePort }, [readablePort]); 121 | const writer = writable.getWriter(); 122 | // ... 123 | 124 | // worker.js 125 | import { fromReadablePort } from 'remote-web-streams'; 126 | self.onmessage = (event) => { 127 | const { readablePort } = event.data; 128 | const reader = readable.getReader(); 129 | // ... 130 | } 131 | ``` 132 | 133 | ## Examples 134 | 135 | ### Remote transform stream 136 | In the basic setup, we create one pair of streams and transfer one end to the worker. 137 | However, it's also possible to set up multiple pairs and transfer them all to a worker. 138 | 139 | This opens up interesting possibilities. We can use a `RemoteWritableStream` to write chunks to a worker, 140 | let the worker transform them using one or more `TransformStream`s, and then read those transformed chunks 141 | back on the main thread using a `RemoteReadableStream`. 142 | This allows us to move one or more CPU-intensive `TransformStream`s off the main thread, 143 | and turn them into a "remote transform stream". 144 | 145 | To demonstrate these "remote transform streams", we set one up to solve the original problem statement: 146 | 1. Create a `RemoteReadableStream` and a `RemoteWritableStream` on the main thread. 147 | 2. Transfer both streams to the worker. Inside the worker, connect the `readable` to the `writable` by piping it 148 | through one or more `TransformStream`s. 149 | 3. On the main thread, write data to be transformed into the `writable` and read transformed data from the `readable`. 150 | Pro-tip: we can use `.pipeThrough({ readable, writable })` for this! 151 | 152 | ```js 153 | // main.js 154 | import { RemoteReadableStream, RemoteWritableStream } from 'remote-web-streams'; 155 | (async () => { 156 | const worker = new Worker('./worker.js', { type: 'module' }); 157 | // create a stream to send the input to the worker 158 | const { writable, readablePort } = new RemoteWritableStream(); 159 | // create a stream to receive the output from the worker 160 | const { readable, writablePort } = new RemoteReadableStream(); 161 | // transfer the other ends to the worker 162 | worker.postMessage({ readablePort, writablePort }, [readablePort, writablePort]); 163 | 164 | const response = await fetch('./some-data.txt'); 165 | await response.body 166 | // send the downloaded data to the worker 167 | // and receive the results back 168 | .pipeThrough({ readable, writable }) 169 | // show the results as they come in 170 | .pipeTo(new WritableStream({ 171 | write(chunk) { 172 | const results = document.getElementById('results'); 173 | results.appendChild(document.createTextNode(chunk)); // tadaa! 174 | } 175 | })); 176 | })(); 177 | 178 | // worker.js 179 | import { fromReadablePort, fromWritablePort } from 'remote-web-streams'; 180 | self.onmessage = async (event) => { 181 | // create the input and output streams from the transferred ports 182 | const { readablePort, writablePort } = event.data; 183 | const readable = fromReadablePort(readablePort); 184 | const writable = fromWritablePort(writablePort); 185 | 186 | // process data 187 | await readable 188 | .pipeThrough(new TransformStream({ 189 | transform(chunk, controller) { 190 | controller.enqueue(process(chunk)); // do the actual work 191 | } 192 | })) 193 | .pipeTo(writable); // send the results back to main thread 194 | }; 195 | ``` 196 | With this set up, we achieve the desired goals: 197 | * Data is transformed as soon as it arrives on the main thread. 198 | * Transformed data is displayed on the web page as soon as it is transformed by the worker. 199 | * All of the data processing happens inside the worker, so it never blocks the main thread. 200 | 201 | The results are shown as fast as possible, and your web page stays snappy. Great success! 🎉 202 | 203 | ## Behind the scenes 204 | The library works its magic by creating a `MessageChannel` between the `WritableStream` and the `ReadableStream`. 205 | The writable end sends a message to the readable end whenever a new chunk is written, 206 | so the readable end can enqueue it for reading. 207 | Similarly, the readable end sends a message to the writable end whenever it needs more data, 208 | so the writable end can release any backpressure. 209 | 210 | [streams-spec]: https://streams.spec.whatwg.org/ 211 | [identity-transform-stream]: https://streams.spec.whatwg.org/#identity-transform-stream 212 | -------------------------------------------------------------------------------- /docs/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Remote web stream examples 6 | 7 | 8 | 9 | 10 | Processing an array inside a web page script 11 | 12 | 13 | Processing an stream inside a web page script 14 | 15 | 16 | Processing an array inside a web worker 17 | 18 | 19 | Processing an stream inside a web worker 20 | 21 | 22 | 23 | Processing an stream inside a web worker using native transferable streams 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/examples/page-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Processing an array inside a web page script 6 | 7 | 8 | 9 | 10 | 11 | This example processes an array of inputs inside a page script. 12 | The page only shows the results after the entire array has been processed, 13 | and the page becomes unresponsive while processing since everything runs on the main thread. 14 | 15 | 16 | JANK METER 17 | 18 | Run 19 | 20 | 21 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/examples/page-stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Processing an stream inside a web page script 6 | 7 | 8 | 9 | 10 | 11 | This example processes an stream of inputs using a TransformStream inside a page script. 12 | A small delay is added between each chunk using setTimeout, such that the page can be repainted 13 | after receiving a result. 14 | However, the page is still unresponsive while processing each chunk, since all processing happens on the main 15 | thread. 16 | 17 | 18 | JANK METER 19 | 20 | Run 21 | 22 | 23 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/examples/process.js: -------------------------------------------------------------------------------- 1 | export function doSomeWork(value) { 2 | let sum = value; 3 | for (let i = 0; i < 1e6; i++) { 4 | sum += Math.random(); 5 | sum -= Math.random(); 6 | } 7 | return sum; 8 | } 9 | 10 | export function processArray(input) { 11 | console.time('process'); 12 | const output = input.map(doSomeWork); 13 | console.timeEnd('process'); 14 | return output; 15 | } 16 | 17 | export function processTransform() { 18 | return new TransformStream({ 19 | start() { 20 | console.time('process'); 21 | }, 22 | transform(chunk, controller) { 23 | controller.enqueue(doSomeWork(chunk)); 24 | }, 25 | flush() { 26 | console.timeEnd('process'); 27 | } 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /docs/examples/resources/jank-meter.css: -------------------------------------------------------------------------------- 1 | #jank-meter { 2 | text-align: center; 3 | animation-direction: alternate; 4 | animation-duration: 1s; 5 | animation-iteration-count: infinite; 6 | animation-timing-function: linear; 7 | animation-name: spacing; 8 | } 9 | 10 | @keyframes spacing { 11 | 0% { 12 | letter-spacing: 0em; 13 | } 14 | 100% { 15 | letter-spacing: 2em; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/examples/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Remote web stream example 6 | 7 | 8 | 9 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/examples/test/worker.js: -------------------------------------------------------------------------------- 1 | import { fromWritablePort } from 'https://unpkg.com/remote-web-streams@0.2.0/dist/remote-web-streams.js'; 2 | 3 | onmessage = async (event) => { 4 | const writable = fromWritablePort(event.data); 5 | const writer = writable.getWriter(); 6 | 7 | try { 8 | const chunks = ['a', 'b', 'c', 'd', 'e']; 9 | for (let chunk of chunks) { 10 | console.log('writer ready'); 11 | await writer.ready; 12 | console.log('writer write:', chunk); 13 | writer.write(chunk).catch(() => {}); 14 | } 15 | console.log('writer close'); 16 | await writer.close(); 17 | } catch (e) { 18 | console.error('writer error:', e); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /docs/examples/utils.js: -------------------------------------------------------------------------------- 1 | export function arrayToStream(array) { 2 | const { readable, writable } = new TransformStream(); 3 | const writer = writable.getWriter(); 4 | for (let item of array) { 5 | writer.write(item); 6 | } 7 | writer.close(); 8 | return readable; 9 | } 10 | 11 | export function delay(ms) { 12 | return new Promise(resolve => setTimeout(resolve, ms)); 13 | } 14 | 15 | export function delayTransform(ms) { 16 | return new TransformStream({ 17 | transform(chunk, controller) { 18 | return delay(ms).then(() => controller.enqueue(chunk)); 19 | } 20 | }); 21 | } 22 | 23 | export function printArrayToElement(element, array) { 24 | element.appendChild(document.createTextNode(array.join('\n') + '\n\n')); 25 | } 26 | 27 | export function printToElementStream(element) { 28 | return new WritableStream({ 29 | write(chunk) { 30 | element.appendChild(document.createTextNode(chunk + '\n')); 31 | }, 32 | close() { 33 | element.appendChild(document.createTextNode('\n')); 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /docs/examples/worker-array.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Processing an array inside a web worker 6 | 7 | 8 | 9 | 10 | 11 | This example processes an array of inputs using a web worker. 12 | The page remains responsive because the processing happens in a separate thread. 13 | However, the page only shows the results after the entire array has been processed. 14 | 15 | 16 | JANK METER 17 | 18 | Run 19 | 20 | 21 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/examples/worker-array.js: -------------------------------------------------------------------------------- 1 | import { processArray } from './process.js'; 2 | 3 | onmessage = (event) => { 4 | const input = event.data; 5 | const output = processArray(input); 6 | self.postMessage(output); 7 | }; 8 | -------------------------------------------------------------------------------- /docs/examples/worker-stream-transferable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Processing an stream inside a web worker with native transferable streams 6 | 7 | 8 | 9 | 10 | 11 | This example processes an stream of inputs using a TransformStream inside a web worker. 12 | It uses transferable ReadableStream and WritableStream to pipe data to and from the 13 | worker, allowing the page to show the results as they are being received 14 | and remain responsive because the processing happens in a separate thread. 15 | 16 | 17 | 18 | This demo requires the browser to support transferable streams. At the time of writing, transferable streams are 19 | available in Chrome Canary 73 with the "experimental Web platform features" flag enabled. 20 | Go to chrome://flags/#enable-experimental-web-platform-features to enabled/disable this flag. 21 | 22 | 23 | JANK METER 24 | 25 | Run 26 | 27 | 28 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/examples/worker-stream-transferable.js: -------------------------------------------------------------------------------- 1 | import { processTransform } from './process.js'; 2 | 3 | onmessage = (event) => { 4 | // retrieve the transferred input and output streams 5 | const [readable, writable] = event.data; 6 | 7 | // transform input and write to output 8 | readable 9 | .pipeThrough(processTransform()) 10 | .pipeTo(writable); 11 | }; 12 | -------------------------------------------------------------------------------- /docs/examples/worker-stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Processing an stream inside a web worker 6 | 7 | 8 | 9 | 10 | 11 | This example processes an stream of inputs using a TransformStream inside a web worker. 12 | It uses RemoteReadableStream and RemoteWritableStream to pipe data to and from the 13 | worker, allowing the page to show the results as they are being received 14 | and remain responsive because the processing happens in a separate thread. 15 | 16 | 17 | JANK METER 18 | 19 | Run 20 | 21 | 22 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/examples/worker-stream.js: -------------------------------------------------------------------------------- 1 | import { processTransform } from './process.js'; 2 | import { 3 | fromReadablePort, 4 | fromWritablePort 5 | } from 'https://unpkg.com/remote-web-streams@0.2.0/dist/remote-web-streams.js'; 6 | 7 | onmessage = (event) => { 8 | // create the input and output streams from the transferred ports 9 | const [readablePort, writablePort] = event.data; 10 | const readable = fromReadablePort(readablePort); 11 | const writable = fromWritablePort(writablePort); 12 | 13 | // transform input and write to output 14 | readable 15 | .pipeThrough(processTransform()) 16 | .pipeTo(writable); 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote-web-streams", 3 | "version": "0.2.0", 4 | "description": "Web streams that work across web workers and iframes.", 5 | "type": "module", 6 | "main": "dist/remote-web-streams.js", 7 | "types": "dist/remote-web-streams.d.ts", 8 | "keywords": [ 9 | "remote", 10 | "web", 11 | "streams", 12 | "readablestream", 13 | "whatwg", 14 | "worker" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/MattiasBuelens/remote-web-streams.git" 19 | }, 20 | "author": "Mattias Buelens", 21 | "license": "MIT", 22 | "engines": { 23 | "node": ">= 18" 24 | }, 25 | "files": [ 26 | "dist/" 27 | ], 28 | "scripts": { 29 | "build": "npm run check:types && npm run build:bundle", 30 | "build:bundle": "rollup -c", 31 | "check:types": "tsc --project ./tsconfig.json --noEmit", 32 | "prepare": "npm run build && npm test", 33 | "start": "rollup -c -w", 34 | "test": "jest" 35 | }, 36 | "devDependencies": { 37 | "@jest/globals": "^29.5.0", 38 | "@rollup/plugin-typescript": "^11.1.1", 39 | "jest": "^29.5.0", 40 | "rollup": "^3.25.3", 41 | "rollup-plugin-dts": "^5.3.0", 42 | "ts-jest": "^29.1.0", 43 | "tslib": "^2.6.0", 44 | "typescript": "^5.1.3" 45 | }, 46 | "jest": { 47 | "transform": { 48 | "^.+\\.tsx?$": [ 49 | "ts-jest", 50 | { 51 | "tsconfig": "test/tsconfig.json" 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from 'rollup-plugin-dts'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | export default [{ 5 | input: './src/index.ts', 6 | output: [{ 7 | file: './dist/remote-web-streams.js', 8 | format: 'es' 9 | }], 10 | plugins: [ 11 | typescript({ 12 | tsconfig: './tsconfig.json' 13 | }) 14 | ] 15 | }, { 16 | input: './src/index.ts', 17 | output: [{ 18 | file: './dist/remote-web-streams.d.ts', 19 | format: 'es' 20 | }], 21 | plugins: [ 22 | dts({ 23 | tsconfig: './tsconfig.json' 24 | }) 25 | ] 26 | }]; 27 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { RemoteWritableStreamOptions as RemoteWritableStreamOptionsType } from './remote'; 2 | 3 | export type RemoteWritableStreamOptions = RemoteWritableStreamOptionsType; 4 | 5 | export { 6 | RemoteReadableStream, 7 | RemoteWritableStream 8 | } from './remote'; 9 | export { fromReadablePort } from './readable'; 10 | export { fromWritablePort } from './writable'; 11 | -------------------------------------------------------------------------------- /src/protocol.ts: -------------------------------------------------------------------------------- 1 | export type SenderType = 'write' | 'abort' | 'close'; 2 | export type ReceiverType = 'pull' | 'error'; 3 | 4 | export interface WriteMessage { 5 | type: 'write', 6 | chunk: any; 7 | } 8 | 9 | export interface AbortMessage { 10 | type: 'abort'; 11 | reason: any; 12 | } 13 | 14 | export interface CloseMessage { 15 | type: 'close'; 16 | } 17 | 18 | export interface PullMessage { 19 | type: 'pull'; 20 | } 21 | 22 | export interface ErrorMessage { 23 | type: 'error'; 24 | reason: any; 25 | } 26 | 27 | export type SenderMessage = WriteMessage | AbortMessage | CloseMessage; 28 | export type ReceiverMessage = PullMessage | ErrorMessage; 29 | -------------------------------------------------------------------------------- /src/readable.ts: -------------------------------------------------------------------------------- 1 | import { ReceiverMessage, ReceiverType, SenderMessage, SenderType } from './protocol'; 2 | 3 | export function fromReadablePort(port: MessagePort): ReadableStream { 4 | return new ReadableStream(new MessagePortSource(port)); 5 | } 6 | 7 | export class MessagePortSource implements UnderlyingDefaultSource { 8 | 9 | private _controller!: ReadableStreamDefaultController; 10 | 11 | constructor(private _port: MessagePort) { 12 | this._port.onmessage = (event) => this._onMessage(event.data); 13 | } 14 | 15 | start(controller: ReadableStreamDefaultController) { 16 | this._controller = controller; 17 | } 18 | 19 | pull(controller: ReadableStreamDefaultController) { 20 | const message: ReceiverMessage = { 21 | type: 'pull' 22 | }; 23 | this._port.postMessage(message); 24 | } 25 | 26 | cancel(reason: any) { 27 | const message: ReceiverMessage = { 28 | type: 'error', 29 | reason 30 | }; 31 | this._port.postMessage(message); 32 | this._port.close(); 33 | } 34 | 35 | private _onMessage(message: SenderMessage) { 36 | switch (message.type) { 37 | case 'write': 38 | // enqueue() will call pull() if needed when there's no backpressure 39 | this._controller.enqueue(message.chunk); 40 | break; 41 | case 'abort': 42 | this._controller.error(message.reason); 43 | this._port.close(); 44 | break; 45 | case 'close': 46 | this._controller.close(); 47 | this._port.close(); 48 | break; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/remote.ts: -------------------------------------------------------------------------------- 1 | import { fromReadablePort } from './readable'; 2 | import { fromWritablePort, MessagePortSinkOptions } from './writable'; 3 | 4 | export class RemoteReadableStream { 5 | 6 | readonly writablePort: MessagePort; 7 | readonly readable: ReadableStream; 8 | 9 | constructor() { 10 | const channel = new MessageChannel(); 11 | this.writablePort = channel.port1; 12 | this.readable = fromReadablePort(channel.port2); 13 | } 14 | 15 | } 16 | 17 | export interface RemoteWritableStreamOptions extends MessagePortSinkOptions { 18 | } 19 | 20 | export class RemoteWritableStream { 21 | 22 | readonly readablePort: MessagePort; 23 | readonly writable: WritableStream; 24 | 25 | constructor(options?: RemoteWritableStreamOptions) { 26 | const channel = new MessageChannel(); 27 | this.readablePort = channel.port1; 28 | this.writable = fromWritablePort(channel.port2, options); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/transfer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns which parts of a chunk should be transferred to the remote end. 3 | * 4 | * @param chunk The chunk to be sent 5 | * @return An array of {@link Transferable transferable} chunk parts 6 | */ 7 | export type TransferChunkCallback = (chunk: T) => Transferable[]; 8 | -------------------------------------------------------------------------------- /src/writable.ts: -------------------------------------------------------------------------------- 1 | import { ReceiverMessage, SenderMessage } from './protocol'; 2 | import { TransferChunkCallback } from './transfer'; 3 | 4 | export interface MessagePortSinkOptions { 5 | transferChunk?: TransferChunkCallback; 6 | } 7 | 8 | export function fromWritablePort(port: MessagePort, 9 | options?: MessagePortSinkOptions): WritableStream { 10 | return new WritableStream(new MessagePortSink(port, options)); 11 | } 12 | 13 | export class MessagePortSink implements UnderlyingSink { 14 | 15 | private readonly _transferChunk?: TransferChunkCallback; 16 | 17 | private _controller!: WritableStreamDefaultController; 18 | 19 | private _readyPromise!: Promise; 20 | private _readyResolve!: () => void; 21 | private _readyReject!: (reason: any) => void; 22 | private _readyPending!: boolean; 23 | 24 | constructor(private readonly _port: MessagePort, options: MessagePortSinkOptions = {}) { 25 | this._transferChunk = options.transferChunk; 26 | this._resetReady(); 27 | this._port.onmessage = (event) => this._onMessage(event.data); 28 | } 29 | 30 | start(controller: WritableStreamDefaultController) { 31 | this._controller = controller; 32 | 33 | // Apply initial backpressure 34 | return this._readyPromise; 35 | } 36 | 37 | write(chunk: W, controller: WritableStreamDefaultController) { 38 | const message: SenderMessage = { 39 | type: 'write', 40 | chunk 41 | }; 42 | // Send chunk, optionally transferring its contents 43 | let transferList: Transferable[] = this._transferChunk ? this._transferChunk(chunk) : []; 44 | if (transferList.length) { 45 | this._port.postMessage(message, transferList); 46 | } else { 47 | this._port.postMessage(message); 48 | } 49 | // Assume backpressure after every write, until sender pulls 50 | this._resetReady(); 51 | // Apply backpressure 52 | return this._readyPromise; 53 | } 54 | 55 | close() { 56 | const message: SenderMessage = { 57 | type: 'close' 58 | }; 59 | this._port.postMessage(message); 60 | this._port.close(); 61 | } 62 | 63 | abort(reason: any) { 64 | const message: SenderMessage = { 65 | type: 'abort', 66 | reason 67 | }; 68 | this._port.postMessage(message); 69 | this._port.close(); 70 | } 71 | 72 | private _onMessage(message: ReceiverMessage) { 73 | switch (message.type) { 74 | case 'pull': 75 | this._resolveReady(); 76 | break; 77 | case 'error': 78 | this._onError(message.reason); 79 | break; 80 | } 81 | } 82 | 83 | private _onError(reason: any) { 84 | this._controller.error(reason); 85 | this._rejectReady(reason); 86 | this._port.close(); 87 | } 88 | 89 | private _resetReady() { 90 | this._readyPromise = new Promise((resolve, reject) => { 91 | this._readyResolve = resolve; 92 | this._readyReject = reject; 93 | }); 94 | this._readyPending = true; 95 | } 96 | 97 | private _resolveReady() { 98 | this._readyResolve(); 99 | this._readyPending = false; 100 | } 101 | 102 | private _rejectReady(reason: any) { 103 | if (!this._readyPending) { 104 | this._resetReady(); 105 | } 106 | this._readyPromise.catch(() => {}); 107 | this._readyReject(reason); 108 | this._readyPending = false; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /test/mocks/EventTarget.ts: -------------------------------------------------------------------------------- 1 | export class MockEventTarget implements EventTarget { 2 | 3 | private readonly _delegateEventTarget: EventTarget; 4 | 5 | constructor() { 6 | // Cannot create or extend EventTarget in jsdom, since it's not attached to a document 7 | // Instead, delegate to another light-weight EventTarget instance 8 | // See https://github.com/jsdom/jsdom/issues/2173 9 | this._delegateEventTarget = new AbortController().signal; 10 | } 11 | 12 | addEventListener(type: string, 13 | listener: EventListenerOrEventListenerObject | null, 14 | options?: boolean | AddEventListenerOptions): void { 15 | this._delegateEventTarget.addEventListener(type, listener, options); 16 | } 17 | 18 | dispatchEvent(evt: Event): boolean { 19 | return this._delegateEventTarget.dispatchEvent(evt); 20 | } 21 | 22 | removeEventListener(type: string, 23 | listener: EventListenerOrEventListenerObject | null, 24 | options?: boolean | EventListenerOptions): void { 25 | this._delegateEventTarget.removeEventListener(type, listener, options); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /test/mocks/MessageChannel.ts: -------------------------------------------------------------------------------- 1 | import { MockEventTarget } from './EventTarget'; 2 | 3 | export class MockMessageChannel implements MessageChannel { 4 | readonly port1: MessagePort; 5 | readonly port2: MessagePort; 6 | 7 | constructor() { 8 | const port1 = new MockMessagePort(); 9 | const port2 = new MockMessagePort(); 10 | port1._entangle(port2); 11 | port2._entangle(port1); 12 | this.port1 = port1; 13 | this.port2 = port2; 14 | } 15 | } 16 | 17 | export type MessageEventHandler = (this: MessagePort, ev: MessageEvent) => any; 18 | 19 | export class MockMessagePort extends MockEventTarget implements MessagePort { 20 | private _messageHandler: MessageEventHandler | null = null; 21 | private _messageHandlerListening: boolean = false; 22 | 23 | private _other: MockMessagePort | undefined = undefined; 24 | private _queue: any[] = []; 25 | private _started: boolean = false; 26 | 27 | _entangle(other: MockMessagePort) { 28 | this._other = other; 29 | } 30 | 31 | start(): void { 32 | if (this._started) { 33 | return; 34 | } 35 | this._started = true; 36 | const queue = this._queue.slice(); 37 | this._queue.length = 0; 38 | for (let message of queue) { 39 | this._dispatchMessageEventAsync(message); 40 | } 41 | } 42 | 43 | postMessage(message: any): void { 44 | this._other!._receiveMessage(message); 45 | } 46 | 47 | close(): void { 48 | this._other = undefined; 49 | this._queue.length = 0; 50 | } 51 | 52 | get onmessage(): MessageEventHandler | null { 53 | return this._messageHandler; 54 | } 55 | 56 | set onmessage(handler: MessageEventHandler | null) { 57 | this._messageHandler = handler; 58 | if (!this._messageHandlerListening) { 59 | this.addEventListener('message', this._messageListener); 60 | this._messageHandlerListening = true; 61 | } 62 | // The first time a MessagePort object's onmessage IDL attribute is set, 63 | // the port's port message queue must be enabled, as if the start() method had been called. 64 | // https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports:handler-messageport-onmessage-2 65 | this.start(); 66 | } 67 | 68 | private _messageListener = (event: MessageEvent) => { 69 | if (this._messageHandler) { 70 | this._messageHandler(event); 71 | } 72 | }; 73 | 74 | private _receiveMessage(message: any) { 75 | if (this._started) { 76 | this._dispatchMessageEventAsync(message); 77 | } else { 78 | this._queue.push(message); 79 | } 80 | } 81 | 82 | private _dispatchMessageEventAsync(message: any) { 83 | setTimeout(this._dispatchMessageEventSync, 0, message); 84 | } 85 | 86 | private _dispatchMessageEventSync = (message: any) => { 87 | const event = new MessageEvent('message', { data: message }); 88 | this.dispatchEvent(event); 89 | }; 90 | 91 | } 92 | 93 | // Copied from lib.dom.d.ts 94 | export interface MockMessagePort extends MessagePort { 95 | 96 | addEventListener(type: K, listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; 97 | 98 | addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; 99 | 100 | removeEventListener(type: K, listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, options?: boolean | EventListenerOptions): void; 101 | 102 | removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; 103 | 104 | } 105 | -------------------------------------------------------------------------------- /test/mocks/dom.ts: -------------------------------------------------------------------------------- 1 | import { MockMessageChannel, MockMessagePort } from './MessageChannel'; 2 | 3 | globalThis.MessageChannel = MockMessageChannel; 4 | globalThis.MessagePort = MockMessagePort; 5 | -------------------------------------------------------------------------------- /test/promise-utils.ts: -------------------------------------------------------------------------------- 1 | export function isPending(promise: Promise): Promise { 2 | const sentinel = {}; 3 | return Promise.race([promise, Promise.resolve(sentinel)]) 4 | .then( 5 | (value) => value === sentinel, 6 | (reason) => false 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /test/remote.spec.ts: -------------------------------------------------------------------------------- 1 | import './mocks/dom'; 2 | import { describe, expect, it, jest } from '@jest/globals'; 3 | import { 4 | fromReadablePort, 5 | fromWritablePort, 6 | RemoteReadableStream, 7 | RemoteWritableStream, 8 | RemoteWritableStreamOptions 9 | } from '../src'; 10 | import { isPending } from './promise-utils'; 11 | 12 | describe('RemoteReadableStream', () => { 13 | 14 | it('constructs', () => { 15 | const stream = new RemoteReadableStream(); 16 | expect(stream).toBeInstanceOf(RemoteReadableStream); 17 | expect(stream.readable).toBeInstanceOf(ReadableStream); 18 | expect(stream.writablePort).toBeInstanceOf(MessagePort); 19 | }); 20 | 21 | tests((options?: RemoteWritableStreamOptions) => { 22 | const stream = new RemoteReadableStream(); 23 | const writable = fromWritablePort(stream.writablePort, options); 24 | return [stream.readable, writable]; 25 | }); 26 | 27 | }); 28 | 29 | describe('RemoteWritableStream', () => { 30 | 31 | it('constructs', () => { 32 | const stream = new RemoteWritableStream(); 33 | expect(stream).toBeInstanceOf(RemoteWritableStream); 34 | expect(stream.writable).toBeInstanceOf(WritableStream); 35 | expect(stream.readablePort).toBeInstanceOf(MessagePort); 36 | }); 37 | 38 | tests((options?: RemoteWritableStreamOptions) => { 39 | const stream = new RemoteWritableStream(options); 40 | const readable = fromReadablePort(stream.readablePort); 41 | return [readable, stream.writable]; 42 | }); 43 | 44 | }); 45 | 46 | // Should behave like a no-op TransformStream 47 | describe('TransformStream (reference)', () => { 48 | 49 | tests((options: RemoteWritableStreamOptions = {}) => { 50 | const stream = new TransformStream({ 51 | transform(chunk, controller) { 52 | if (options.transferChunk) { 53 | options.transferChunk(chunk); 54 | } 55 | controller.enqueue(chunk); 56 | } 57 | }); 58 | return [stream.readable, stream.writable]; 59 | }); 60 | 61 | }); 62 | 63 | type SetupFn = (options?: RemoteWritableStreamOptions) => [ReadableStream, WritableStream]; 64 | 65 | function tests(setup: SetupFn) { 66 | 67 | it('reads chunks from writable', async () => { 68 | const [readable, writable] = setup(); 69 | const reader = readable.getReader(); 70 | const writer = writable.getWriter(); 71 | 72 | const read1 = reader.read(); 73 | const read2 = reader.read(); 74 | const read3 = reader.read(); 75 | void writer.write('a'); 76 | void writer.write('b'); 77 | void writer.write('c'); 78 | 79 | await expect(read1).resolves.toEqual({ done: false, value: 'a' }); 80 | await expect(read2).resolves.toEqual({ done: false, value: 'b' }); 81 | await expect(read3).resolves.toEqual({ done: false, value: 'c' }); 82 | }); 83 | 84 | it('respects backpressure', async () => { 85 | const [readable, writable] = setup(); 86 | const reader = readable.getReader(); 87 | const writer = writable.getWriter(); 88 | 89 | const ready1 = writer.ready; 90 | const write1 = writer.write('a'); 91 | const ready2 = writer.ready; 92 | const write2 = writer.write('b'); 93 | const ready3 = writer.ready; 94 | await Promise.all([ 95 | expect(ready1).resolves.toBe(undefined), 96 | expect(isPending(write1)).resolves.toBe(true), 97 | expect(isPending(ready2)).resolves.toBe(true), 98 | expect(isPending(write2)).resolves.toBe(true), 99 | expect(isPending(ready3)).resolves.toBe(true) 100 | ]); 101 | 102 | const read1 = reader.read(); 103 | await Promise.all([ 104 | expect(read1).resolves.toEqual({ done: false, value: 'a' }), 105 | expect(write1).resolves.toBe(undefined), 106 | expect(isPending(ready2)).resolves.toBe(true), 107 | expect(isPending(write2)).resolves.toBe(true), 108 | expect(isPending(ready3)).resolves.toBe(true) 109 | ]); 110 | 111 | const read2 = reader.read(); 112 | await Promise.all([ 113 | expect(read2).resolves.toEqual({ done: false, value: 'b' }), 114 | expect(write2).resolves.toBe(undefined), 115 | expect(isPending(ready2)).resolves.toBe(true), 116 | expect(isPending(ready3)).resolves.toBe(true) 117 | ]); 118 | 119 | await Promise.all([ 120 | expect(ready2).resolves.toBe(undefined), 121 | expect(ready3).resolves.toBe(undefined) 122 | ]); 123 | }); 124 | 125 | it('propagates close', async () => { 126 | const [readable, writable] = setup(); 127 | const reader = readable.getReader(); 128 | const writer = writable.getWriter(); 129 | 130 | void writer.write('a'); 131 | void writer.write('b'); 132 | void writer.close(); 133 | 134 | const read1 = reader.read(); 135 | const read2 = reader.read(); 136 | const read3 = reader.read(); 137 | await expect(read1).resolves.toEqual({ done: false, value: 'a' }); 138 | await expect(read2).resolves.toEqual({ done: false, value: 'b' }); 139 | await expect(read3).resolves.toEqual({ done: true, value: undefined }); 140 | await expect(reader.closed).resolves.toBe(undefined); 141 | }); 142 | 143 | it('cancels readable when writable aborts', async () => { 144 | const [readable, writable] = setup(); 145 | const reader = readable.getReader(); 146 | const writer = writable.getWriter(); 147 | 148 | const reason = 'oops'; 149 | void writer.write('a').catch(() => {}); 150 | void writer.abort(reason); 151 | 152 | const read1 = reader.read(); 153 | await expect(read1).rejects.toBe(reason); 154 | await expect(reader.closed).rejects.toBe(reason); 155 | await expect(writer.closed).rejects.toBe(reason); 156 | }); 157 | 158 | it('aborts writable when readable cancels', async () => { 159 | const [readable, writable] = setup(); 160 | const reader = readable.getReader(); 161 | const writer = writable.getWriter(); 162 | 163 | const reason = 'never mind'; 164 | const read1 = reader.read(); 165 | void reader.cancel(reason); 166 | 167 | const write1 = writer.write('a'); 168 | await expect(read1).resolves.toEqual({ done: true, value: undefined }); 169 | await expect(write1).rejects.toBe(reason); 170 | await expect(reader.closed).resolves.toBe(undefined); 171 | await expect(writer.closed).rejects.toBe(reason); 172 | }); 173 | 174 | it('uses transferChunk callback', async () => { 175 | const transferChunk = jest.fn((chunk: Uint8Array) => [chunk.buffer]); 176 | const [readable, writable] = setup({ transferChunk }); 177 | const reader = readable.getReader(); 178 | const writer = writable.getWriter(); 179 | 180 | const read1 = reader.read(); 181 | 182 | const chunk1 = new Uint8Array([1]); 183 | await writer.write(chunk1); 184 | expect(transferChunk).toHaveBeenLastCalledWith(chunk1); 185 | 186 | await expect(read1).resolves.toEqual({ done: false, value: chunk1 }); 187 | }); 188 | 189 | } 190 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "module": "commonjs", 6 | "esModuleInterop": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2021", 4 | "module": "es2020", 5 | "moduleResolution": "node", 6 | "isolatedModules": true, 7 | "lib": [ 8 | "dom", 9 | "es2021" 10 | ], 11 | "strict": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------
11 | This example processes an array of inputs inside a page script. 12 | The page only shows the results after the entire array has been processed, 13 | and the page becomes unresponsive while processing since everything runs on the main thread. 14 |
11 | This example processes an stream of inputs using a TransformStream inside a page script. 12 | A small delay is added between each chunk using setTimeout, such that the page can be repainted 13 | after receiving a result. 14 | However, the page is still unresponsive while processing each chunk, since all processing happens on the main 15 | thread. 16 |
TransformStream
setTimeout
11 | This example processes an array of inputs using a web worker. 12 | The page remains responsive because the processing happens in a separate thread. 13 | However, the page only shows the results after the entire array has been processed. 14 |
11 | This example processes an stream of inputs using a TransformStream inside a web worker. 12 | It uses transferable ReadableStream and WritableStream to pipe data to and from the 13 | worker, allowing the page to show the results as they are being received 14 | and remain responsive because the processing happens in a separate thread. 15 |
ReadableStream
WritableStream
18 | This demo requires the browser to support transferable streams. At the time of writing, transferable streams are 19 | available in Chrome Canary 73 with the "experimental Web platform features" flag enabled. 20 | Go to chrome://flags/#enable-experimental-web-platform-features to enabled/disable this flag. 21 |
chrome://flags/#enable-experimental-web-platform-features
11 | This example processes an stream of inputs using a TransformStream inside a web worker. 12 | It uses RemoteReadableStream and RemoteWritableStream to pipe data to and from the 13 | worker, allowing the page to show the results as they are being received 14 | and remain responsive because the processing happens in a separate thread. 15 |
RemoteReadableStream
RemoteWritableStream