Browse Source

Add puter.js

KernelDeimos 1 năm trước cách đây
mục cha
commit
b8e66cada9
51 tập tin đã thay đổi với 12551 bổ sung3 xóa
  1. 24 0
      packages/backend/src/SelfhostedModule.js
  2. 93 0
      packages/backend/src/services/DevWatcherService.js
  3. 17 0
      packages/backend/src/services/ServceStaticFilesService.js
  4. 19 0
      packages/backend/src/util/configutil.js
  5. 137 0
      packages/puter-dot-js/.gitignore
  6. 201 0
      packages/puter-dot-js/APACHE_LICENSE.txt
  7. 55 0
      packages/puter-dot-js/README.md
  8. 12 0
      packages/puter-dot-js/app-migration-guide.txt
  9. 1738 0
      packages/puter-dot-js/package-lock.json
  10. 19 0
      packages/puter-dot-js/package.json
  11. BIN
      packages/puter-dot-js/src/bg.png
  12. BIN
      packages/puter-dot-js/src/bg.webp
  13. 358 0
      packages/puter-dot-js/src/index.js
  14. 49 0
      packages/puter-dot-js/src/lib/EventListener.js
  15. 509 0
      packages/puter-dot-js/src/lib/path.js
  16. 5 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.esm.min.js
  17. 0 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.esm.min.js.map
  18. 4385 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.js
  19. 0 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.js.map
  20. 5 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.min.js
  21. 0 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.min.js.map
  22. 5 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.msgpack.min.js
  23. 0 0
      packages/puter-dot-js/src/lib/socket.io/socket.io.msgpack.min.js.map
  24. 436 0
      packages/puter-dot-js/src/lib/utils.js
  25. 253 0
      packages/puter-dot-js/src/modules/AI.js
  26. 158 0
      packages/puter-dot-js/src/modules/Apps.js
  27. 115 0
      packages/puter-dot-js/src/modules/Auth.js
  28. 98 0
      packages/puter-dot-js/src/modules/FSItem.js
  29. 133 0
      packages/puter-dot-js/src/modules/FileSystem/index.js
  30. 61 0
      packages/puter-dot-js/src/modules/FileSystem/operations/copy.js
  31. 59 0
      packages/puter-dot-js/src/modules/FileSystem/operations/deleteFSEntry.js
  32. 59 0
      packages/puter-dot-js/src/modules/FileSystem/operations/mkdir.js
  33. 57 0
      packages/puter-dot-js/src/modules/FileSystem/operations/move.js
  34. 44 0
      packages/puter-dot-js/src/modules/FileSystem/operations/read.js
  35. 46 0
      packages/puter-dot-js/src/modules/FileSystem/operations/readdir.js
  36. 56 0
      packages/puter-dot-js/src/modules/FileSystem/operations/rename.js
  37. 103 0
      packages/puter-dot-js/src/modules/FileSystem/operations/sign.js
  38. 40 0
      packages/puter-dot-js/src/modules/FileSystem/operations/space.js
  39. 57 0
      packages/puter-dot-js/src/modules/FileSystem/operations/stat.js
  40. 419 0
      packages/puter-dot-js/src/modules/FileSystem/operations/upload.js
  41. 53 0
      packages/puter-dot-js/src/modules/FileSystem/operations/write.js
  42. 21 0
      packages/puter-dot-js/src/modules/FileSystem/utils/getAbsolutePathForApp.js
  43. 136 0
      packages/puter-dot-js/src/modules/Hosting.js
  44. 205 0
      packages/puter-dot-js/src/modules/KV.js
  45. 90 0
      packages/puter-dot-js/src/modules/OS.js
  46. 27 0
      packages/puter-dot-js/src/modules/PuterDialog.js
  47. 1019 0
      packages/puter-dot-js/src/modules/UI.js
  48. 619 0
      packages/puter-dot-js/test/fs.test.js
  49. 417 0
      packages/puter-dot-js/test/kv.test.js
  50. 137 0
      packages/puter-dot-js/test/run.html
  51. 2 3
      src/index.js

+ 24 - 0
packages/backend/src/SelfhostedModule.js

@@ -9,6 +9,30 @@ class SelfhostedModule extends AdvancedBase {
 
         const ComplainAboutVersionsService = require('./services/ComplainAboutVersionsService');
         services.registerService('complain-about-versions', ComplainAboutVersionsService);
+
+        const DevWatcherService = require('./services/DevWatcherService');
+        const path_ = require('path');
+        services.registerService('__dev-watcher', DevWatcherService, {
+            root: path_.resolve(__dirname, '../../../'),
+            commands: [
+                {
+                    name: 'puter.js:webpack-watch',
+                    directory: 'packages/puter-dot-js',
+                    command: 'npm',
+                    args: ['run', 'start-webpack'],
+                },
+            ],
+        });
+
+        const ServeStaticFilesService = require("./services/ServceStaticFilesService");
+        services.registerService('__serve-puterjs', ServeStaticFilesService, {
+            directories: [
+                {
+                    prefix: '/sdk',
+                    path: path_.resolve(__dirname, '../../../packages/puter-dot-js/dist'),
+                },
+            ],
+        });
     }
 }
 

+ 93 - 0
packages/backend/src/services/DevWatcherService.js

@@ -0,0 +1,93 @@
+const BaseService = require("./BaseService");
+
+class ProxyLogger {
+    constructor (log) {
+        this.log = log;
+    }
+    attach (stream) {
+        let buffer = '';
+        stream.on('data', (chunk) => {
+            buffer += chunk.toString();
+            let lineEndIndex = buffer.indexOf('\n');
+            while (lineEndIndex !== -1) {
+                const line = buffer.substring(0, lineEndIndex);
+                this.log(line);
+                buffer = buffer.substring(lineEndIndex + 1);
+                lineEndIndex = buffer.indexOf('\n');
+            }
+        });
+
+        stream.on('end', () => {
+            if (buffer.length) {
+                this.log(buffer);
+            }
+        });
+    }
+}
+
+/**
+ * @description
+ * This service is used to run webpack watchers.
+ */
+class DevWatcherService extends BaseService {
+    static MODULES = {
+        path: require('path'),
+        spawn: require('child_process').spawn,
+    };
+
+    _construct () {
+        this.instances = [];
+    }
+
+    async _init (args) {
+        const { root, commands } = args;
+
+        process.on('exit', () => {
+            this.exit_all_();
+        })
+
+        for ( const entry of commands ) {
+            const { name, directory, command, args } = entry;
+            const fullpath = this.modules.path.join(
+                root, directory);
+            this.start_({ name, fullpath, command, args });
+        }
+    }
+
+    log_ (name, isErr, line) {
+        let txt = `[${name}:`;
+        txt += isErr
+            ? `\x1B[31;1merr\x1B[0m`
+            : `\x1B[32;1mout\x1B[0m`;
+        txt += '] ' + line;
+        this.log.info(txt);
+    }
+
+    async start_ ({ name, fullpath, command, args }) {
+        this.log.info(`Starting ${name} in ${fullpath}`);
+        const proc = this.modules.spawn(command, args, {
+            shell: true,
+            env: process.env,
+            cwd: fullpath,
+        });
+        this.instances.push({
+            name, proc,
+        });
+        const out = new ProxyLogger((line) => this.log_(name, false, line));
+        out.attach(proc.stdout);
+        const err = new ProxyLogger((line) => this.log_(name, true, line));
+        err.attach(proc.stderr);
+        proc.on('exit', () => {
+            this.log.info(`[${name}:exit] Process exited (${proc.exitCode})`);
+            this.instances = this.instances.filter((inst) => inst.proc !== proc);
+        })
+    }
+
+    async exit_all_ () {
+        for ( const { proc } of this.instances ) {
+            proc.kill();
+        }
+    }
+};
+
+module.exports = DevWatcherService;

+ 17 - 0
packages/backend/src/services/ServceStaticFilesService.js

@@ -0,0 +1,17 @@
+const BaseService = require("./BaseService");
+
+class ServeStaticFilesService extends BaseService {
+    async _init (args) {
+        this.directories = args.directories;
+    }
+
+    async ['__on_install.routes'] () {
+        const { app } = this.services.get('web-server');
+
+        for ( const { prefix, path } of this.directories ) {
+            app.use(prefix, require('express').static(path));
+        }
+    }
+}
+
+module.exports = ServeStaticFilesService;

+ 19 - 0
packages/backend/src/util/configutil.js

@@ -0,0 +1,19 @@
+let memoized_common_template_vars_ = null;
+const get_common_template_vars = () => {
+    const path_ = require('path');
+    if ( memoized_common_template_vars_ !== null ) {
+        return memoized_common_template_vars_;
+    }
+
+    const code_root = path_.resolve(__dirname, '../../');
+
+    memoized_common_template_vars_ = {
+        code_root,
+    };
+
+    return memoized_common_template_vars_;
+}
+
+module.exports = {
+    get_common_template_vars,
+};

+ 137 - 0
packages/puter-dot-js/.gitignore

@@ -0,0 +1,137 @@
+# MAC OS hidden directory settings file
+.DS_Store
+
+# Created by https://www.toptal.com/developers/gitignore/api/node
+# Edit at https://www.toptal.com/developers/gitignore?templates=node
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+.env.production
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# End of https://www.toptal.com/developers/gitignore/api/node
+*.zip
+*.pem
+.DS_Store
+./build
+build
+
+# config file
+src/config.js
+ssl
+ssl/

+ 201 - 0
packages/puter-dot-js/APACHE_LICENSE.txt

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2024 Puter Technologies Inc. All Rights Reserved.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 55 - 0
packages/puter-dot-js/README.md

@@ -0,0 +1,55 @@
+<h3 align="center">Puter.js</h3>
+<h4 align="center">The official JavaScript SDK for Puter.com. Cloud and AI features right from your frontend code!</h4>
+<p align="center">
+    <a href="https://docs.puter.com/playground/"><strong>« LIVE DEMO »</strong></a>
+    <br />
+    <br />
+    <a href="https://docs.puter.com" target="_blank">Docs</a>
+    ·
+    <a href="https://puter.com">Puter.com</a>
+    ·
+    <a href="https://discord.com/invite/PQcx7Teh8u">Discord</a>
+    ·
+    <a href="https://reddit.com/r/puter">Reddit</a>
+    ·
+    <a href="https://twitter.com/HeyPuter">X (Twitter)</a>
+</p>
+
+## Installation
+```
+git clone https://github.com/HeyPuter/puter.js.git
+cd puter.js
+npm install
+```
+
+## Run development server
+
+```
+npm start
+```
+
+## Build
+
+```
+npm run build
+```
+
+### Example
+Make sure the development server is running.
+
+```html
+<html>
+<body>
+    <script src="http://127.0.0.1:8080/dist/puter.dev.js"></script>
+    <script>
+        // Loading ...
+        puter.print(`Loading...`);
+
+        // Chat with GPT-3.5 Turbo
+        puter.ai.chat(`What color was Napoleon's white horse?`).then((response) => {
+            puter.print(response);
+        });
+    </script>
+</body>
+</html>
+```

+ 12 - 0
packages/puter-dot-js/app-migration-guide.txt

@@ -0,0 +1,12 @@
+all UI function calls from puter should be in the form `puter.ui.<function>`
+    e.g. `puter.showOpenFilePicker` becomes `puter.ui.showOpenFilePicker`
+
+puter.FileSystem.<function> -> puter.fs.<function>
+puter.Router.<function> -> puter.router.<function>
+puter.Apps.<function> -> puter.apps.<function>
+
+puter.setItem -> puter.kv.set
+puter.getItem -> puter.kv.get
+puter.removeItem -> puter.kv.del
+puter.createCloudItem(...) -> new puter.CloudItem(...)
+puter.router.* -> puter.hosting.*

+ 1738 - 0
packages/puter-dot-js/package-lock.json

@@ -0,0 +1,1738 @@
+{
+  "name": "puter",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "puter",
+      "version": "1.0.0",
+      "license": "ISC",
+      "devDependencies": {
+        "concurrently": "^8.2.2",
+        "webpack-cli": "^5.1.4"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.23.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz",
+      "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==",
+      "dev": true,
+      "dependencies": {
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@discoveryjs/json-ext": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+      "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/source-map": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+      "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.0",
+        "@jridgewell/trace-mapping": "^0.3.9"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.4.15",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.20",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
+      "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@types/eslint": {
+      "version": "8.44.8",
+      "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz",
+      "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/estree": "*",
+        "@types/json-schema": "*"
+      }
+    },
+    "node_modules/@types/eslint-scope": {
+      "version": "3.7.7",
+      "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+      "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/eslint": "*",
+        "@types/estree": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@types/node": {
+      "version": "20.10.2",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.2.tgz",
+      "integrity": "sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@webassemblyjs/ast": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+      "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+      "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+      "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+      "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+      "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+        "@webassemblyjs/helper-api-error": "1.11.6",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+      "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+      "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/ieee754": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+      "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/@webassemblyjs/leb128": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+      "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/utf8": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+      "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+      "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/helper-wasm-section": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6",
+        "@webassemblyjs/wasm-opt": "1.11.6",
+        "@webassemblyjs/wasm-parser": "1.11.6",
+        "@webassemblyjs/wast-printer": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+      "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/ieee754": "1.11.6",
+        "@webassemblyjs/leb128": "1.11.6",
+        "@webassemblyjs/utf8": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+      "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-buffer": "1.11.6",
+        "@webassemblyjs/wasm-gen": "1.11.6",
+        "@webassemblyjs/wasm-parser": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+      "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@webassemblyjs/helper-api-error": "1.11.6",
+        "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+        "@webassemblyjs/ieee754": "1.11.6",
+        "@webassemblyjs/leb128": "1.11.6",
+        "@webassemblyjs/utf8": "1.11.6"
+      }
+    },
+    "node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.11.6",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+      "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@webassemblyjs/ast": "1.11.6",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webpack-cli/configtest": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+      "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.15.0"
+      },
+      "peerDependencies": {
+        "webpack": "5.x.x",
+        "webpack-cli": "5.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/info": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+      "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.15.0"
+      },
+      "peerDependencies": {
+        "webpack": "5.x.x",
+        "webpack-cli": "5.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/serve": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+      "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=14.15.0"
+      },
+      "peerDependencies": {
+        "webpack": "5.x.x",
+        "webpack-cli": "5.x.x"
+      },
+      "peerDependenciesMeta": {
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/@xtuc/long": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/acorn": {
+      "version": "8.11.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+      "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+      "dev": true,
+      "peer": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-import-assertions": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+      "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
+      "dev": true,
+      "peer": true,
+      "peerDependencies": {
+        "acorn": "^8"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-keywords": {
+      "version": "3.5.2",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+      "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+      "dev": true,
+      "peer": true,
+      "peerDependencies": {
+        "ajv": "^6.9.1"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
+      "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "peer": true,
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001541",
+        "electron-to-chromium": "^1.4.535",
+        "node-releases": "^2.0.13",
+        "update-browserslist-db": "^1.0.13"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001565",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
+      "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "peer": true
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chalk/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/chrome-trace-event": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
+      "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/clone-deep": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+      "dev": true,
+      "dependencies": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "dev": true
+    },
+    "node_modules/concurrently": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz",
+      "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^4.1.2",
+        "date-fns": "^2.30.0",
+        "lodash": "^4.17.21",
+        "rxjs": "^7.8.1",
+        "shell-quote": "^1.8.1",
+        "spawn-command": "0.0.2",
+        "supports-color": "^8.1.1",
+        "tree-kill": "^1.2.2",
+        "yargs": "^17.7.2"
+      },
+      "bin": {
+        "conc": "dist/bin/concurrently.js",
+        "concurrently": "dist/bin/concurrently.js"
+      },
+      "engines": {
+        "node": "^14.13.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/date-fns": {
+      "version": "2.30.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
+      "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
+      "dev": true,
+      "dependencies": {
+        "@babel/runtime": "^7.21.0"
+      },
+      "engines": {
+        "node": ">=0.11"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/date-fns"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.4.601",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.601.tgz",
+      "integrity": "sha512-SpwUMDWe9tQu8JX5QCO1+p/hChAi9AE9UpoC3rcHVc+gdCGlbT3SGb5I1klgb952HRIyvt9wZhSz9bNBYz9swA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.15.0",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+      "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/envinfo": {
+      "version": "7.11.0",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz",
+      "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==",
+      "dev": true,
+      "bin": {
+        "envinfo": "dist/cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
+      "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/escalade": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esrecurse/node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/fastest-levenshtein": {
+      "version": "1.0.16",
+      "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+      "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4.9.1"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true,
+      "bin": {
+        "flat": "cli.js"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "dev": true,
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/import-local": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+      "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+      "dev": true,
+      "dependencies": {
+        "pkg-dir": "^4.2.0",
+        "resolve-cwd": "^3.0.0"
+      },
+      "bin": {
+        "import-local-fixture": "fixtures/cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/interpret": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+      "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+      "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+      "dev": true,
+      "dependencies": {
+        "hasown": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "dependencies": {
+        "isobject": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true
+    },
+    "node_modules/isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/loader-runner": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+      "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6.11.5"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "node_modules/rechoir": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+      "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+      "dev": true,
+      "dependencies": {
+        "resolve": "^1.20.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/regenerator-runtime": {
+      "version": "0.14.0",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
+      "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
+      "dev": true
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-cwd": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+      "dev": true,
+      "dependencies": {
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/rxjs": {
+      "version": "7.8.1",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+      "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+      "dev": true,
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "peer": true
+    },
+    "node_modules/schema-utils": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+      "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/json-schema": "^7.0.8",
+        "ajv": "^6.12.5",
+        "ajv-keywords": "^3.5.2"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/serialize-javascript": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+      "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
+    "node_modules/shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+      "dev": true,
+      "dependencies": {
+        "kind-of": "^6.0.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/spawn-command": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz",
+      "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
+      "dev": true
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tapable": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+      "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/terser": {
+      "version": "5.24.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
+      "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.3",
+        "acorn": "^8.8.2",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/terser-webpack-plugin": {
+      "version": "5.3.9",
+      "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+      "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.17",
+        "jest-worker": "^27.4.5",
+        "schema-utils": "^3.1.1",
+        "serialize-javascript": "^6.0.1",
+        "terser": "^5.16.8"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "uglify-js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/terser/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/tree-kill": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+      "dev": true,
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+      "dev": true
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "dev": true,
+      "peer": true
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
+      "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "peer": true,
+      "dependencies": {
+        "escalade": "^3.1.1",
+        "picocolors": "^1.0.0"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/watchpack": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
+      "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/webpack": {
+      "version": "5.89.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
+      "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
+      "dev": true,
+      "peer": true,
+      "dependencies": {
+        "@types/eslint-scope": "^3.7.3",
+        "@types/estree": "^1.0.0",
+        "@webassemblyjs/ast": "^1.11.5",
+        "@webassemblyjs/wasm-edit": "^1.11.5",
+        "@webassemblyjs/wasm-parser": "^1.11.5",
+        "acorn": "^8.7.1",
+        "acorn-import-assertions": "^1.9.0",
+        "browserslist": "^4.14.5",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.15.0",
+        "es-module-lexer": "^1.2.1",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.2.9",
+        "json-parse-even-better-errors": "^2.3.1",
+        "loader-runner": "^4.2.0",
+        "mime-types": "^2.1.27",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^3.2.0",
+        "tapable": "^2.1.1",
+        "terser-webpack-plugin": "^5.3.7",
+        "watchpack": "^2.4.0",
+        "webpack-sources": "^3.2.3"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+      "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+      "dev": true,
+      "dependencies": {
+        "@discoveryjs/json-ext": "^0.5.0",
+        "@webpack-cli/configtest": "^2.1.1",
+        "@webpack-cli/info": "^2.0.2",
+        "@webpack-cli/serve": "^2.0.5",
+        "colorette": "^2.0.14",
+        "commander": "^10.0.1",
+        "cross-spawn": "^7.0.3",
+        "envinfo": "^7.7.3",
+        "fastest-levenshtein": "^1.0.12",
+        "import-local": "^3.0.2",
+        "interpret": "^3.1.1",
+        "rechoir": "^0.8.0",
+        "webpack-merge": "^5.7.3"
+      },
+      "bin": {
+        "webpack-cli": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=14.15.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "5.x.x"
+      },
+      "peerDependenciesMeta": {
+        "@webpack-cli/generators": {
+          "optional": true
+        },
+        "webpack-bundle-analyzer": {
+          "optional": true
+        },
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli/node_modules/commander": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+      "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+      "dev": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/webpack-merge": {
+      "version": "5.10.0",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
+      "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+      "dev": true,
+      "dependencies": {
+        "clone-deep": "^4.0.1",
+        "flat": "^5.0.2",
+        "wildcard": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+      "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+      "dev": true,
+      "peer": true,
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wildcard": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+      "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+      "dev": true
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      }
+    }
+  }
+}

+ 19 - 0
packages/puter-dot-js/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "puter",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "start-server": "npx http-server --cors -c-1",
+    "start-webpack": "export NODE_OPTIONS=--openssl-legacy-provider && webpack ./src/index.js --output-filename puter.js && webpack ./src/index.js --output-filename puter.dev.js --watch --devtool source-map",
+    "start": "concurrently \"npm run start-server\" \"npm run start-webpack\"",
+    "build": "export NODE_OPTIONS=--openssl-legacy-provider && webpack ./src/index.js --output-filename puter.js && { echo \"// Copyright 2024 Puter Technologies Inc. All rights reserved.\"; echo \"// Generated on $(date '+%Y-%m-%d %H:%M')\n\"; cat ./dist/puter.js; } > temp && mv temp ./dist/puter.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "Apache-2.0",
+  "devDependencies": {
+    "concurrently": "^8.2.2",
+    "webpack-cli": "^5.1.4"
+  }
+}

BIN
packages/puter-dot-js/src/bg.png


BIN
packages/puter-dot-js/src/bg.webp


+ 358 - 0
packages/puter-dot-js/src/index.js

@@ -0,0 +1,358 @@
+import OS from './modules/OS.js';
+import FileSystem from './modules/FileSystem/index.js';
+import Hosting from './modules/Hosting.js';
+import Apps from './modules/Apps.js';
+import UI from './modules/UI.js';
+import KV from './modules/KV.js';
+import AI from './modules/AI.js';
+import Auth from './modules/Auth.js';
+import FSItem from './modules/FSItem.js';
+import * as utils from './lib/utils.js';
+import path from './lib/path.js';
+
+window.puter = (function() {
+    'use strict';
+
+    class Puter{
+        // The environment that the SDK is running in. Can be 'gui', 'app' or 'web'.
+        // 'gui' means the SDK is running in the Puter GUI, i.e. Puter.com.
+        // 'app' means the SDK is running as a Puter app, i.e. within an iframe in the Puter GUI.
+        // 'web' means the SDK is running in a 3rd-party website.
+        env;
+
+        defaultAPIOrigin = 'https://api.puter.com';
+        defaultGUIOrigin = 'https://puter.com';
+
+        // An optional callback when the user is authenticated. This can be set by the app using the SDK.
+        onAuth;
+
+        /**
+         * State object to keep track of the authentication request status.
+         * This is used to prevent multiple authentication popups from showing up by different parts of the app.
+         */
+        puterAuthState = {
+            isPromptOpen: false,
+            authGranted: null,
+            resolver: null
+        };
+
+        // Holds the unique app instance ID that is provided by the host environment
+        appInstanceID;
+
+        // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
+        parentInstanceID;
+
+        // Expose the FSItem class
+        static FSItem = FSItem;
+
+        // Event handling properties
+        eventHandlers = {};
+
+        // --------------------------------------------
+        // Constructor
+        // --------------------------------------------
+        constructor(options) {
+            options = options ?? {};
+
+            // Holds the query parameters found in the current URL
+            let URLParams = new URLSearchParams(window.location.search);
+
+            // Figure out the environment in which the SDK is running
+            if (URLParams.has('puter.app_instance_id'))
+                this.env = 'app';
+            else if(window.puter_gui_enabled === true)
+                this.env = 'gui';
+            else
+                this.env = 'web';
+
+            // there are some specific situations where puter is definitely loaded in GUI mode
+            // we're going to check for those situations here so that we don't break anything unintentionally
+            // if navigator URL's hostname is 'puter.com'
+            if(window.location.hostname === 'puter.com'){
+                this.env = 'gui';
+            }
+
+            // Get the 'args' from the URL. This is used to pass arguments to the app.
+            if(URLParams.has('puter.args')){
+                this.args = JSON.parse(decodeURIComponent(URLParams.get('puter.args')));
+            }else{
+                this.args = {};
+            }
+
+            // Try to extract appInstanceID from the URL. appInstanceID is included in every messaage
+            // sent to the host environment. This is used to help host environment identify the app
+            // instance that sent the message and communicate back to it.
+            if(URLParams.has('puter.app_instance_id')){
+                this.appInstanceID = decodeURIComponent(URLParams.get('puter.app_instance_id'));
+            }
+
+            // Try to extract parentInstanceID from the URL. If another app launched this app instance, parentInstanceID
+            // holds its instance ID, and is used to communicate with that parent app.
+            if(URLParams.has('puter.parent_instance_id')){
+                this.parentInstanceID = decodeURIComponent(URLParams.get('puter.parent_instance_id'));
+            }
+
+            // Try to extract `puter.app.id` from the URL. `puter.app.id` is the unique ID of the app.
+            // App ID is useful for identifying the app when communicating with the Puter API, among other things.
+            if(URLParams.has('puter.app.id')){
+                this.appID = decodeURIComponent(URLParams.get('puter.app.id'));
+            }
+
+            // Construct this App's AppData path based on the appID. AppData path is used to store files that are specific to this app.
+            // The default AppData path is `~/AppData/<appID>`.
+            if(this.appID){
+                this.appDataPath = `~/AppData/${this.appID}`;
+            }
+
+            // Construct APIOrigin from the URL. APIOrigin is used to build the URLs for the Puter API endpoints.
+            // The default APIOrigin is https://api.puter.com. However, if the URL contains a `puter.api_origin` query parameter,
+            // then that value is used as the APIOrigin. If the URL contains a `puter.domain` query parameter, then the APIOrigin
+            // is constructed as `https://api.<puter.domain>`.
+            this.APIOrigin = this.defaultAPIOrigin;
+            if(URLParams.has('puter.api_origin')){
+                this.APIOrigin = decodeURIComponent(URLParams.get('puter.api_origin'));
+            }else if(URLParams.has('puter.domain')){
+                this.APIOrigin = 'https://api.' + URLParams.get('puter.domain');
+            }
+
+            // The SDK is running in the Puter GUI (i.e. 'gui')
+            if(this.env === 'gui'){
+                this.authToken = window.auth_token;
+                // initialize submodules
+                this.initSubmodules();
+            }
+            // Loaded in an iframe in the Puter GUI (i.e. 'app')
+            // When SDK is loaded in App mode the initiation process should start when the DOM is ready
+            else if (this.env === 'app') {
+                this.authToken = decodeURIComponent(URLParams.get('puter.auth.token'));
+                // initialize submodules
+                this.initSubmodules();
+                // If the authToken is already set in localStorage, then we don't need to show the dialog
+                try {
+                    if(localStorage.getItem('puter.auth.token')){
+                        this.setAuthToken(localStorage.getItem('puter.auth.token'));
+                    }
+                    // if appID is already set in localStorage, then we don't need to show the dialog
+                    if(localStorage.getItem('puter.app.id')){
+                        this.setAppID(localStorage.getItem('puter.app.id'));
+                    }
+                } catch (error) {
+                    // Handle the error here
+                    console.error('Error accessing localStorage:', error);
+                }
+            }
+            // SDK was loaded in a 3rd-party website.
+            // When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
+            // the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
+            else if(this.env === 'web') {
+                // initialize submodules
+                this.initSubmodules();
+                try{
+                    // If the authToken is already set in localStorage, then we don't need to show the dialog
+                    if(localStorage.getItem('puter.auth.token')){
+                        this.setAuthToken(localStorage.getItem('puter.auth.token'));
+                    }
+                    // if appID is already set in localStorage, then we don't need to show the dialog
+                    if(localStorage.getItem('puter.app.id')){
+                        this.setAppID(localStorage.getItem('puter.app.id'));
+                    }
+                } catch (error) {
+                    // Handle the error here
+                    console.error('Error accessing localStorage:', error);
+                }
+            }
+        }
+
+        // Initialize submodules
+        initSubmodules = function(){
+            // Auth
+            this.auth = new Auth(this.authToken, this.APIOrigin, this.appID, this.env);
+            // OS
+            this.os = new OS(this.authToken, this.APIOrigin, this.appID, this.env);
+            // FileSystem
+            this.fs = new FileSystem(this.authToken, this.APIOrigin, this.appID, this.env);
+            // UI
+            this.ui = new UI(this.appInstanceID, this.parentInstanceID, this.appID, this.env);
+            // Hosting
+            this.hosting = new Hosting(this.authToken, this.APIOrigin, this.appID, this.env);
+            // Apps
+            this.apps = new Apps(this.authToken, this.APIOrigin, this.appID, this.env);
+            // AI
+            this.ai = new AI(this.authToken, this.APIOrigin, this.appID, this.env);
+            // Key-Value Store
+            this.kv = new KV(this.authToken, this.APIOrigin, this.appID, this.env);
+            // Path
+            this.path = path;
+        }
+
+        updateSubmodules() {
+            // Update submodules with new auth token and API origin
+            [this.os, this.fs, this.hosting, this.apps, this.ai, this.kv].forEach(module => {
+                if(!module) return;
+                module.setAuthToken(this.authToken);
+                module.setAPIOrigin(this.APIOrigin);
+            });
+        }
+
+        setAppID = function (appID) {
+            // save to localStorage
+            try{
+                localStorage.setItem('puter.app.id', appID);
+            } catch (error) {
+                // Handle the error here
+                console.error('Error accessing localStorage:', error);
+            }
+            this.appID = appID;
+        }
+
+        setAuthToken = function (authToken) {
+            this.authToken = authToken;
+            // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
+            if(this.env === 'web' || this.env === 'app'){
+                try{
+                    localStorage.setItem('puter.auth.token', authToken);
+                } catch (error) {
+                    // Handle the error here
+                    console.error('Error accessing localStorage:', error);
+                }
+            }
+            // reinitialize submodules
+            this.updateSubmodules();
+        }
+
+        setAPIOrigin = function (APIOrigin) {
+            this.APIOrigin = APIOrigin;
+            // reinitialize submodules
+            this.updateSubmodules();
+        }
+
+        resetAuthToken = function () {
+            this.authToken = null;
+            // If the SDK is running on a 3rd-party site or an app, then save the authToken in localStorage
+            if(this.env === 'web' || this.env === 'app'){
+                try{
+                    localStorage.removeItem('puter.auth.token');
+                } catch (error) {
+                    // Handle the error here
+                    console.error('Error accessing localStorage:', error);
+                }
+            }
+            // reinitialize submodules
+            this.updateSubmodules();
+        }
+
+        exit = function() {
+            window.parent.postMessage({
+                msg: "exit",
+                appInstanceID: this.appInstanceID,
+            }, '*');
+        }
+
+        /**
+         * A function that generates a domain-safe name by combining a random adjective, a random noun, and a random number (between 0 and 9999).
+         * The result is returned as a string with components separated by hyphens.
+         * It is useful when you need to create unique identifiers that are also human-friendly.
+         *
+         * @param {string} [separateWith='-'] - The character to use to separate the components of the generated name.
+         * @returns {string} A unique, hyphen-separated string comprising of an adjective, a noun, and a number.
+         *
+         */
+        randName = function(separateWith = '-'){
+            const first_adj = ['helpful','sensible', 'loyal', 'honest', 'clever', 'capable','calm', 'smart', 'genius', 'bright', 'charming', 'creative', 'diligent', 'elegant', 'fancy', 
+            'colorful', 'avid', 'active', 'gentle', 'happy', 'intelligent', 'jolly', 'kind', 'lively', 'merry', 'nice', 'optimistic', 'polite', 
+            'quiet', 'relaxed', 'silly', 'victorious', 'witty', 'young', 'zealous', 'strong', 'brave', 'agile', 'bold'];
+
+            const nouns = ['street', 'roof', 'floor', 'tv', 'idea', 'morning', 'game', 'wheel', 'shoe', 'bag', 'clock', 'pencil', 'pen', 
+            'magnet', 'chair', 'table', 'house', 'dog', 'room', 'book', 'car', 'cat', 'tree', 
+            'flower', 'bird', 'fish', 'sun', 'moon', 'star', 'cloud', 'rain', 'snow', 'wind', 'mountain', 
+            'river', 'lake', 'sea', 'ocean', 'island', 'bridge', 'road', 'train', 'plane', 'ship', 'bicycle', 
+            'horse', 'elephant', 'lion', 'tiger', 'bear', 'zebra', 'giraffe', 'monkey', 'snake', 'rabbit', 'duck', 
+            'goose', 'penguin', 'frog', 'crab', 'shrimp', 'whale', 'octopus', 'spider', 'ant', 'bee', 'butterfly', 'dragonfly', 
+            'ladybug', 'snail', 'camel', 'kangaroo', 'koala', 'panda', 'piglet', 'sheep', 'wolf', 'fox', 'deer', 'mouse', 'seal',
+            'chicken', 'cow', 'dinosaur', 'puppy', 'kitten', 'circle', 'square', 'garden', 'otter', 'bunny', 'meerkat', 'harp']
+
+            // return a random combination of first_adj + noun + number (between 0 and 9999)
+            // e.g. clever-idea-123
+            return first_adj[Math.floor(Math.random() * first_adj.length)] + separateWith + nouns[Math.floor(Math.random() * nouns.length)] + separateWith + Math.floor(Math.random() * 10000);
+        }
+
+        getUser = function(...args){
+            let options;
+    
+            // If first argument is an object, it's the options
+            if (typeof args[0] === 'object' && args[0] !== null) {
+                options = args[0];
+            } else {
+                // Otherwise, we assume separate arguments are provided
+                options = {
+                    success: args[0],
+                    error: args[1],
+                };
+            }
+    
+            return new Promise((resolve, reject) => {
+                const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
+    
+                // set up event handlers for load and error events
+                utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+    
+                xhr.send();
+            })
+        }
+
+        print = function(...args){
+            for(let arg of args){
+                document.getElementsByTagName('body')[0].append(arg);
+            }
+        }
+    }
+
+
+    // Create a new Puter object and return it
+    const puterobj = new Puter();
+
+    // Return the Puter object
+    return puterobj;
+}());
+
+window.addEventListener('message', async (event) => {
+    // if the message is not from Puter, then ignore it
+    if(event.origin !== puter.defaultGUIOrigin) return;
+
+    if(event.data.msg && event.data.msg === 'requestOrigin'){
+        event.source.postMessage({
+            msg: "originResponse",
+        }, '*');    
+    }
+    else if (event.data.msg === 'puter.token') {
+        // puterDialog.close();
+        // Set the authToken property
+        puter.setAuthToken(event.data.token);
+        // update appID
+        puter.setAppID(event.data.app_uid);
+        // Remove the event listener to avoid memory leaks
+        // window.removeEventListener('message', messageListener);
+
+        puter.puterAuthState.authGranted = true;
+        // Resolve the promise
+        // resolve();
+
+        // Call onAuth callback
+        if(puter.onAuth && typeof puter.onAuth === 'function'){
+            puter.getUser().then((user) => {
+                puter.onAuth(user)
+            });
+        }
+
+        puter.puterAuthState.isPromptOpen = false;
+        // Resolve or reject any waiting promises.
+        if (puter.puterAuthState.resolver) {
+            if (puter.puterAuthState.authGranted) {
+                puter.puterAuthState.resolver.resolve();
+            } else {
+                puter.puterAuthState.resolver.reject();
+            }
+            puter.puterAuthState.resolver = null;
+        };
+    }
+})

+ 49 - 0
packages/puter-dot-js/src/lib/EventListener.js

@@ -0,0 +1,49 @@
+export default class EventListener {
+    // Array of all supported event names.
+    #eventNames;
+
+    // Map of eventName -> array of listeners
+    #eventListeners;
+
+    constructor(eventNames) {
+        this.#eventNames = eventNames;
+
+        this.#eventListeners = (() => {
+            const map = new Map();
+            for (let eventName of this.#eventNames) {
+                map[eventName] = [];
+            }
+            return map;
+        })();
+    }
+
+    emit(eventName, data) {
+        if (!this.#eventNames.includes(eventName)) {
+            console.error(`Event name '${eventName}' not supported`);
+            return;
+        }
+        this.#eventListeners[eventName].forEach((listener) => {
+            listener(data);
+        });
+    }
+
+    on(eventName, callback) {
+        if (!this.#eventNames.includes(eventName)) {
+            console.error(`Event name '${eventName}' not supported`);
+            return;
+        }
+        this.#eventListeners[eventName].push(callback);
+    }
+
+    off(eventName, callback) {
+        if (!this.#eventNames.includes(eventName)) {
+            console.error(`Event name '${eventName}' not supported`);
+            return;
+        }
+        const listeners = this.#eventListeners[eventName];
+        const index = listeners.indexOf(callback)
+        if (index !== -1) {
+            listeners.splice(index, 1);
+        }
+    }
+}

+ 509 - 0
packages/puter-dot-js/src/lib/path.js

@@ -0,0 +1,509 @@
+// import {cwd} from './env.js'
+let cwd;
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+//'use strict';
+
+const
+  CHAR_UPPERCASE_A = 65,
+  CHAR_LOWERCASE_A = 97,
+  CHAR_UPPERCASE_Z = 90,
+  CHAR_LOWERCASE_Z = 122,
+  CHAR_DOT = 46,
+  CHAR_FORWARD_SLASH = 47,
+  CHAR_BACKWARD_SLASH = 92,
+  CHAR_COLON = 58,
+  CHAR_QUESTION_MARK = 63;
+
+function isPathSeparator(code) {
+	return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH;
+}
+
+function isPosixPathSeparator(code) {
+	return code === CHAR_FORWARD_SLASH;
+}
+
+// Resolves . and .. elements in a path with directory names
+function normalizeString(path, allowAboveRoot, separator, isPathSeparator) {
+  let res = '';
+  let lastSegmentLength = 0;
+  let lastSlash = -1;
+  let dots = 0;
+  let code = 0;
+  for (let i = 0; i <= path.length; ++i) {
+	if (i < path.length)
+	  code = path.charCodeAt(i);
+	else if (isPathSeparator(code))
+	  break;
+	else
+	  code = CHAR_FORWARD_SLASH;
+
+	if (isPathSeparator(code)) {
+	  if (lastSlash === i - 1 || dots === 1) {
+		// NOOP
+	  } else if (dots === 2) {
+		if (res.length < 2 || lastSegmentLength !== 2 ||
+			res.charCodeAt( res.length - 1) !== CHAR_DOT ||
+			res.charCodeAt(res.length - 2) !== CHAR_DOT) {
+		  if (res.length > 2) {
+			const lastSlashIndex = res.lastIndexOf(separator);
+			if (lastSlashIndex === -1) {
+			  res = '';
+			  lastSegmentLength = 0;
+			} else {
+			  res = res.slice(0, lastSlashIndex);
+			  lastSegmentLength =
+				res.length - 1 - res.lastIndexOf(res, separator);
+			}
+			lastSlash = i;
+			dots = 0;
+			continue;
+		  } else if (res.length !== 0) {
+			res = '';
+			lastSegmentLength = 0;
+			lastSlash = i;
+			dots = 0;
+			continue;
+		  }
+		}
+		if (allowAboveRoot) {
+		  res += res.length > 0 ? `${separator}..` : '..';
+		  lastSegmentLength = 2;
+		}
+	  } else {
+		if (res.length > 0)
+		  res += `${separator}${path.slice(lastSlash + 1, i)}`;
+		else
+		  res = path.slice(lastSlash + 1, i);
+		lastSegmentLength = i - lastSlash - 1;
+	  }
+	  lastSlash = i;
+	  dots = 0;
+	} else if (code === CHAR_DOT && dots !== -1) {
+	  ++dots;
+	} else {
+	  dots = -1;
+	}
+  }
+  return res;
+}
+
+const path = {
+    // path.resolve([from ...], to)
+    resolve(...args) {
+      let resolvedPath = '';
+      let resolvedAbsolute = false;
+  
+      for (let i = args.length - 1; i >= -1 && !resolvedAbsolute; i--) {
+        // orig const path = i >= 0 ? args[i] : posixCwd();
+        const path = i >= 0 ? args[i] : (cwd !== undefined ? cwd : '/');
+        // const path = i >= 0 ? args[i] : '/';
+  
+        // Skip empty entries
+        if (path.length === 0) {
+          continue;
+        }
+  
+        resolvedPath = `${path}/${resolvedPath}`;
+        resolvedAbsolute =
+          path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+      }
+  
+      // At this point the path should be resolved to a full absolute path, but
+      // handle relative paths to be safe (might happen when process.cwd() fails)
+  
+      // Normalize the path
+      resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, '/',
+                                     isPosixPathSeparator);
+  
+      if (resolvedAbsolute) {
+        return `/${resolvedPath}`;
+      }
+      return resolvedPath.length > 0 ? resolvedPath : '.';
+    },
+  
+    normalize(path) {
+      if (path.length === 0)
+        return '.';
+  
+      const isAbsolute =
+        path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+      const trailingSeparator =
+        path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;
+  
+      // Normalize the path
+      path = normalizeString(path, !isAbsolute, '/', isPosixPathSeparator);
+  
+      if (path.length === 0) {
+        if (isAbsolute)
+          return '/';
+        return trailingSeparator ? './' : '.';
+      }
+      if (trailingSeparator)
+        path += '/';
+  
+      return isAbsolute ? `/${path}` : path;
+    },
+  
+    isAbsolute(path) {
+      return path.length > 0 &&
+             path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+    },
+  
+    join(...args) {
+      if (args.length === 0)
+        return '.';
+      let joined;
+      for (let i = 0; i < args.length; ++i) {
+        const arg = args[i];
+        if (arg.length > 0) {
+          if (joined === undefined)
+            joined = arg;
+          else
+            joined += `/${arg}`;
+        }
+      }
+      if (joined === undefined)
+        return '.';
+      return path.normalize(joined);
+    },
+  
+    relative(from, to) {
+      if (from === to)
+        return '';
+  
+      // Trim leading forward slashes.
+      from = path.resolve(from);
+      to = path.resolve(to);
+  
+      if (from === to)
+        return '';
+  
+      const fromStart = 1;
+      const fromEnd = from.length;
+      const fromLen = fromEnd - fromStart;
+      const toStart = 1;
+      const toLen = to.length - toStart;
+  
+      // Compare paths to find the longest common path from root
+      const length = (fromLen < toLen ? fromLen : toLen);
+      let lastCommonSep = -1;
+      let i = 0;
+      for (; i < length; i++) {
+        const fromCode = from.charCodeAt(fromStart + i);
+        if (fromCode !== to.charCodeAt(toStart + i))
+          break;
+        else if (fromCode === CHAR_FORWARD_SLASH)
+          lastCommonSep = i;
+      }
+      if (i === length) {
+        if (toLen > length) {
+          if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
+            // We get here if `from` is the exact base path for `to`.
+            // For example: from='/foo/bar'; to='/foo/bar/baz'
+            return to.slice(toStart + i + 1);
+          }
+          if (i === 0) {
+            // We get here if `from` is the root
+            // For example: from='/'; to='/foo'
+            return to.slice(toStart + i);
+          }
+        } else if (fromLen > length) {
+          if (from.charCodeAt(fromStart + i) ===
+              CHAR_FORWARD_SLASH) {
+            // We get here if `to` is the exact base path for `from`.
+            // For example: from='/foo/bar/baz'; to='/foo/bar'
+            lastCommonSep = i;
+          } else if (i === 0) {
+            // We get here if `to` is the root.
+            // For example: from='/foo/bar'; to='/'
+            lastCommonSep = 0;
+          }
+        }
+      }
+  
+      let out = '';
+      // Generate the relative path based on the path difference between `to`
+      // and `from`.
+      for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
+        if (i === fromEnd ||
+            from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+          out += out.length === 0 ? '..' : '/..';
+        }
+      }
+  
+      // Lastly, append the rest of the destination (`to`) path that comes after
+      // the common path parts.
+      return `${out}${to.slice(toStart + lastCommonSep)}`;
+    },
+  
+    toNamespacedPath(path) {
+      // Non-op on posix systems
+      return path;
+    },
+  
+    dirname(path) {
+      if (path.length === 0)
+        return '.';
+      const hasRoot = path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+      let end = -1;
+      let matchedSlash = true;
+      for (let i = path.length - 1; i >= 1; --i) {
+        if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+          if (!matchedSlash) {
+            end = i;
+            break;
+          }
+        } else {
+          // We saw the first non-path separator
+          matchedSlash = false;
+        }
+      }
+  
+      if (end === -1)
+        return hasRoot ? '/' : '.';
+      if (hasRoot && end === 1)
+        return '//';
+      return path.slice(0, end);
+    },
+  
+    basename(path, ext) {
+      let start = 0;
+      let end = -1;
+      let matchedSlash = true;
+  
+      if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
+        if (ext === path)
+          return '';
+        let extIdx = ext.length - 1;
+        let firstNonSlashEnd = -1;
+        for (let i = path.length - 1; i >= 0; --i) {
+          const code = path.charCodeAt(i);
+          if (code === CHAR_FORWARD_SLASH) {
+            // If we reached a path separator that was not part of a set of path
+            // separators at the end of the string, stop now
+            if (!matchedSlash) {
+              start = i + 1;
+              break;
+            }
+          } else {
+            if (firstNonSlashEnd === -1) {
+              // We saw the first non-path separator, remember this index in case
+              // we need it if the extension ends up not matching
+              matchedSlash = false;
+              firstNonSlashEnd = i + 1;
+            }
+            if (extIdx >= 0) {
+              // Try to match the explicit extension
+              if (code === ext.charCodeAt(extIdx)) {
+                if (--extIdx === -1) {
+                  // We matched the extension, so mark this as the end of our path
+                  // component
+                  end = i;
+                }
+              } else {
+                // Extension does not match, so our result is the entire path
+                // component
+                extIdx = -1;
+                end = firstNonSlashEnd;
+              }
+            }
+          }
+        }
+  
+        if (start === end)
+          end = firstNonSlashEnd;
+        else if (end === -1)
+          end = path.length;
+        return path.slice(start, end);
+      }
+      for (let i = path.length - 1; i >= 0; --i) {
+        if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
+          // If we reached a path separator that was not part of a set of path
+          // separators at the end of the string, stop now
+          if (!matchedSlash) {
+            start = i + 1;
+            break;
+          }
+        } else if (end === -1) {
+          // We saw the first non-path separator, mark this as the end of our
+          // path component
+          matchedSlash = false;
+          end = i + 1;
+        }
+      }
+  
+      if (end === -1)
+        return '';
+      return path.slice(start, end);
+    },
+  
+    extname(path) {
+      let startDot = -1;
+      let startPart = 0;
+      let end = -1;
+      let matchedSlash = true;
+      // Track the state of characters (if any) we see before our first dot and
+      // after any path separator we find
+      let preDotState = 0;
+      for (let i = path.length - 1; i >= 0; --i) {
+        const code = path.charCodeAt(i);
+        if (code === CHAR_FORWARD_SLASH) {
+          // If we reached a path separator that was not part of a set of path
+          // separators at the end of the string, stop now
+          if (!matchedSlash) {
+            startPart = i + 1;
+            break;
+          }
+          continue;
+        }
+        if (end === -1) {
+          // We saw the first non-path separator, mark this as the end of our
+          // extension
+          matchedSlash = false;
+          end = i + 1;
+        }
+        if (code === CHAR_DOT) {
+          // If this is our first dot, mark it as the start of our extension
+          if (startDot === -1)
+            startDot = i;
+          else if (preDotState !== 1)
+            preDotState = 1;
+        } else if (startDot !== -1) {
+          // We saw a non-dot and non-path separator before our dot, so we should
+          // have a good chance at having a non-empty extension
+          preDotState = -1;
+        }
+      }
+  
+      if (startDot === -1 ||
+          end === -1 ||
+          // We saw a non-dot character immediately before the dot
+          preDotState === 0 ||
+          // The (right-most) trimmed path component is exactly '..'
+          (preDotState === 1 &&
+           startDot === end - 1 &&
+           startDot === startPart + 1)) {
+        return '';
+      }
+      return path.slice(startDot, end);
+    },
+  
+    format: _format.bind( null, '/'),
+  
+    parse(path) {
+      const ret = { root: '', dir: '', base: '', ext: '', name: '' };
+      if (path.length === 0)
+        return ret;
+      const isAbsolute =
+        path.charCodeAt(0) === CHAR_FORWARD_SLASH;
+      let start;
+      if (isAbsolute) {
+        ret.root = '/';
+        start = 1;
+      } else {
+        start = 0;
+      }
+      let startDot = -1;
+      let startPart = 0;
+      let end = -1;
+      let matchedSlash = true;
+      let i = path.length - 1;
+  
+      // Track the state of characters (if any) we see before our first dot and
+      // after any path separator we find
+      let preDotState = 0;
+  
+      // Get non-dir info
+      for (; i >= start; --i) {
+        const code = path.charCodeAt(i);
+        if (code === CHAR_FORWARD_SLASH) {
+          // If we reached a path separator that was not part of a set of path
+          // separators at the end of the string, stop now
+          if (!matchedSlash) {
+            startPart = i + 1;
+            break;
+          }
+          continue;
+        }
+        if (end === -1) {
+          // We saw the first non-path separator, mark this as the end of our
+          // extension
+          matchedSlash = false;
+          end = i + 1;
+        }
+        if (code === CHAR_DOT) {
+          // If this is our first dot, mark it as the start of our extension
+          if (startDot === -1)
+            startDot = i;
+          else if (preDotState !== 1)
+            preDotState = 1;
+        } else if (startDot !== -1) {
+          // We saw a non-dot and non-path separator before our dot, so we should
+          // have a good chance at having a non-empty extension
+          preDotState = -1;
+        }
+      }
+  
+      if (end !== -1) {
+        const start = startPart === 0 && isAbsolute ? 1 : startPart;
+        if (startDot === -1 ||
+            // We saw a non-dot character immediately before the dot
+            preDotState === 0 ||
+            // The (right-most) trimmed path component is exactly '..'
+            (preDotState === 1 &&
+            startDot === end - 1 &&
+            startDot === startPart + 1)) {
+          ret.base = ret.name = path.slice(start, end);
+        } else {
+          ret.name = path.slice(start, startDot);
+          ret.base = path.slice(start, end);
+          ret.ext = path.slice(startDot, end);
+        }
+      }
+  
+      if (startPart > 0)
+        ret.dir = path.slice(0, startPart - 1);
+      else if (isAbsolute)
+        ret.dir = '/';
+  
+      return ret;
+    },
+  
+    sep: '/',
+    delimiter: ':',
+    win32: null,
+    posix: null
+  };
+
+  function _format(sep, pathObject) {
+    validateObject(pathObject, 'pathObject');
+    const dir = pathObject.dir || pathObject.root;
+    const base = pathObject.base ||
+      `${pathObject.name || ''}${pathObject.ext || ''}`;
+    if (!dir) {
+      return base;
+    }
+    return dir === pathObject.root ? `${dir}${base}` : `${dir}${sep}${base}`;
+  }
+  
+export default path

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.esm.min.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.esm.min.js.map


+ 4385 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.js

@@ -0,0 +1,4385 @@
+/*!
+ * Socket.IO v4.7.2
+ * (c) 2014-2023 Guillermo Rauch
+ * Released under the MIT License.
+ */
+(function (global, factory) {
+  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+  typeof define === 'function' && define.amd ? define(factory) :
+  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.io = factory());
+})(this, (function () { 'use strict';
+
+  function _typeof(obj) {
+    "@babel/helpers - typeof";
+
+    return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) {
+      return typeof obj;
+    } : function (obj) {
+      return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+    }, _typeof(obj);
+  }
+  function _classCallCheck(instance, Constructor) {
+    if (!(instance instanceof Constructor)) {
+      throw new TypeError("Cannot call a class as a function");
+    }
+  }
+  function _defineProperties(target, props) {
+    for (var i = 0; i < props.length; i++) {
+      var descriptor = props[i];
+      descriptor.enumerable = descriptor.enumerable || false;
+      descriptor.configurable = true;
+      if ("value" in descriptor) descriptor.writable = true;
+      Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
+    }
+  }
+  function _createClass(Constructor, protoProps, staticProps) {
+    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
+    if (staticProps) _defineProperties(Constructor, staticProps);
+    Object.defineProperty(Constructor, "prototype", {
+      writable: false
+    });
+    return Constructor;
+  }
+  function _extends() {
+    _extends = Object.assign ? Object.assign.bind() : function (target) {
+      for (var i = 1; i < arguments.length; i++) {
+        var source = arguments[i];
+        for (var key in source) {
+          if (Object.prototype.hasOwnProperty.call(source, key)) {
+            target[key] = source[key];
+          }
+        }
+      }
+      return target;
+    };
+    return _extends.apply(this, arguments);
+  }
+  function _inherits(subClass, superClass) {
+    if (typeof superClass !== "function" && superClass !== null) {
+      throw new TypeError("Super expression must either be null or a function");
+    }
+    subClass.prototype = Object.create(superClass && superClass.prototype, {
+      constructor: {
+        value: subClass,
+        writable: true,
+        configurable: true
+      }
+    });
+    Object.defineProperty(subClass, "prototype", {
+      writable: false
+    });
+    if (superClass) _setPrototypeOf(subClass, superClass);
+  }
+  function _getPrototypeOf(o) {
+    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
+      return o.__proto__ || Object.getPrototypeOf(o);
+    };
+    return _getPrototypeOf(o);
+  }
+  function _setPrototypeOf(o, p) {
+    _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
+      o.__proto__ = p;
+      return o;
+    };
+    return _setPrototypeOf(o, p);
+  }
+  function _isNativeReflectConstruct() {
+    if (typeof Reflect === "undefined" || !Reflect.construct) return false;
+    if (Reflect.construct.sham) return false;
+    if (typeof Proxy === "function") return true;
+    try {
+      Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
+      return true;
+    } catch (e) {
+      return false;
+    }
+  }
+  function _construct(Parent, args, Class) {
+    if (_isNativeReflectConstruct()) {
+      _construct = Reflect.construct.bind();
+    } else {
+      _construct = function _construct(Parent, args, Class) {
+        var a = [null];
+        a.push.apply(a, args);
+        var Constructor = Function.bind.apply(Parent, a);
+        var instance = new Constructor();
+        if (Class) _setPrototypeOf(instance, Class.prototype);
+        return instance;
+      };
+    }
+    return _construct.apply(null, arguments);
+  }
+  function _isNativeFunction(fn) {
+    return Function.toString.call(fn).indexOf("[native code]") !== -1;
+  }
+  function _wrapNativeSuper(Class) {
+    var _cache = typeof Map === "function" ? new Map() : undefined;
+    _wrapNativeSuper = function _wrapNativeSuper(Class) {
+      if (Class === null || !_isNativeFunction(Class)) return Class;
+      if (typeof Class !== "function") {
+        throw new TypeError("Super expression must either be null or a function");
+      }
+      if (typeof _cache !== "undefined") {
+        if (_cache.has(Class)) return _cache.get(Class);
+        _cache.set(Class, Wrapper);
+      }
+      function Wrapper() {
+        return _construct(Class, arguments, _getPrototypeOf(this).constructor);
+      }
+      Wrapper.prototype = Object.create(Class.prototype, {
+        constructor: {
+          value: Wrapper,
+          enumerable: false,
+          writable: true,
+          configurable: true
+        }
+      });
+      return _setPrototypeOf(Wrapper, Class);
+    };
+    return _wrapNativeSuper(Class);
+  }
+  function _assertThisInitialized(self) {
+    if (self === void 0) {
+      throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+    }
+    return self;
+  }
+  function _possibleConstructorReturn(self, call) {
+    if (call && (typeof call === "object" || typeof call === "function")) {
+      return call;
+    } else if (call !== void 0) {
+      throw new TypeError("Derived constructors may only return object or undefined");
+    }
+    return _assertThisInitialized(self);
+  }
+  function _createSuper(Derived) {
+    var hasNativeReflectConstruct = _isNativeReflectConstruct();
+    return function _createSuperInternal() {
+      var Super = _getPrototypeOf(Derived),
+        result;
+      if (hasNativeReflectConstruct) {
+        var NewTarget = _getPrototypeOf(this).constructor;
+        result = Reflect.construct(Super, arguments, NewTarget);
+      } else {
+        result = Super.apply(this, arguments);
+      }
+      return _possibleConstructorReturn(this, result);
+    };
+  }
+  function _superPropBase(object, property) {
+    while (!Object.prototype.hasOwnProperty.call(object, property)) {
+      object = _getPrototypeOf(object);
+      if (object === null) break;
+    }
+    return object;
+  }
+  function _get() {
+    if (typeof Reflect !== "undefined" && Reflect.get) {
+      _get = Reflect.get.bind();
+    } else {
+      _get = function _get(target, property, receiver) {
+        var base = _superPropBase(target, property);
+        if (!base) return;
+        var desc = Object.getOwnPropertyDescriptor(base, property);
+        if (desc.get) {
+          return desc.get.call(arguments.length < 3 ? target : receiver);
+        }
+        return desc.value;
+      };
+    }
+    return _get.apply(this, arguments);
+  }
+  function _unsupportedIterableToArray(o, minLen) {
+    if (!o) return;
+    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
+    var n = Object.prototype.toString.call(o).slice(8, -1);
+    if (n === "Object" && o.constructor) n = o.constructor.name;
+    if (n === "Map" || n === "Set") return Array.from(o);
+    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
+  }
+  function _arrayLikeToArray(arr, len) {
+    if (len == null || len > arr.length) len = arr.length;
+    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
+    return arr2;
+  }
+  function _createForOfIteratorHelper(o, allowArrayLike) {
+    var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
+    if (!it) {
+      if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
+        if (it) o = it;
+        var i = 0;
+        var F = function () {};
+        return {
+          s: F,
+          n: function () {
+            if (i >= o.length) return {
+              done: true
+            };
+            return {
+              done: false,
+              value: o[i++]
+            };
+          },
+          e: function (e) {
+            throw e;
+          },
+          f: F
+        };
+      }
+      throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
+    }
+    var normalCompletion = true,
+      didErr = false,
+      err;
+    return {
+      s: function () {
+        it = it.call(o);
+      },
+      n: function () {
+        var step = it.next();
+        normalCompletion = step.done;
+        return step;
+      },
+      e: function (e) {
+        didErr = true;
+        err = e;
+      },
+      f: function () {
+        try {
+          if (!normalCompletion && it.return != null) it.return();
+        } finally {
+          if (didErr) throw err;
+        }
+      }
+    };
+  }
+  function _toPrimitive(input, hint) {
+    if (typeof input !== "object" || input === null) return input;
+    var prim = input[Symbol.toPrimitive];
+    if (prim !== undefined) {
+      var res = prim.call(input, hint || "default");
+      if (typeof res !== "object") return res;
+      throw new TypeError("@@toPrimitive must return a primitive value.");
+    }
+    return (hint === "string" ? String : Number)(input);
+  }
+  function _toPropertyKey(arg) {
+    var key = _toPrimitive(arg, "string");
+    return typeof key === "symbol" ? key : String(key);
+  }
+
+  var PACKET_TYPES = Object.create(null); // no Map = no polyfill
+  PACKET_TYPES["open"] = "0";
+  PACKET_TYPES["close"] = "1";
+  PACKET_TYPES["ping"] = "2";
+  PACKET_TYPES["pong"] = "3";
+  PACKET_TYPES["message"] = "4";
+  PACKET_TYPES["upgrade"] = "5";
+  PACKET_TYPES["noop"] = "6";
+  var PACKET_TYPES_REVERSE = Object.create(null);
+  Object.keys(PACKET_TYPES).forEach(function (key) {
+    PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key;
+  });
+  var ERROR_PACKET = {
+    type: "error",
+    data: "parser error"
+  };
+
+  var withNativeBlob$1 = typeof Blob === "function" || typeof Blob !== "undefined" && Object.prototype.toString.call(Blob) === "[object BlobConstructor]";
+  var withNativeArrayBuffer$2 = typeof ArrayBuffer === "function";
+  // ArrayBuffer.isView method is not defined in IE10
+  var isView$1 = function isView(obj) {
+    return typeof ArrayBuffer.isView === "function" ? ArrayBuffer.isView(obj) : obj && obj.buffer instanceof ArrayBuffer;
+  };
+  var encodePacket = function encodePacket(_ref, supportsBinary, callback) {
+    var type = _ref.type,
+      data = _ref.data;
+    if (withNativeBlob$1 && data instanceof Blob) {
+      if (supportsBinary) {
+        return callback(data);
+      } else {
+        return encodeBlobAsBase64(data, callback);
+      }
+    } else if (withNativeArrayBuffer$2 && (data instanceof ArrayBuffer || isView$1(data))) {
+      if (supportsBinary) {
+        return callback(data);
+      } else {
+        return encodeBlobAsBase64(new Blob([data]), callback);
+      }
+    }
+    // plain string
+    return callback(PACKET_TYPES[type] + (data || ""));
+  };
+  var encodeBlobAsBase64 = function encodeBlobAsBase64(data, callback) {
+    var fileReader = new FileReader();
+    fileReader.onload = function () {
+      var content = fileReader.result.split(",")[1];
+      callback("b" + (content || ""));
+    };
+    return fileReader.readAsDataURL(data);
+  };
+  function toArray(data) {
+    if (data instanceof Uint8Array) {
+      return data;
+    } else if (data instanceof ArrayBuffer) {
+      return new Uint8Array(data);
+    } else {
+      return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+    }
+  }
+  var TEXT_ENCODER;
+  function encodePacketToBinary(packet, callback) {
+    if (withNativeBlob$1 && packet.data instanceof Blob) {
+      return packet.data.arrayBuffer().then(toArray).then(callback);
+    } else if (withNativeArrayBuffer$2 && (packet.data instanceof ArrayBuffer || isView$1(packet.data))) {
+      return callback(toArray(packet.data));
+    }
+    encodePacket(packet, false, function (encoded) {
+      if (!TEXT_ENCODER) {
+        TEXT_ENCODER = new TextEncoder();
+      }
+      callback(TEXT_ENCODER.encode(encoded));
+    });
+  }
+
+  // imported from https://github.com/socketio/base64-arraybuffer
+  var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+  // Use a lookup table to find the index.
+  var lookup$1 = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
+  for (var i$1 = 0; i$1 < chars.length; i$1++) {
+    lookup$1[chars.charCodeAt(i$1)] = i$1;
+  }
+  var decode$1 = function decode(base64) {
+    var bufferLength = base64.length * 0.75,
+      len = base64.length,
+      i,
+      p = 0,
+      encoded1,
+      encoded2,
+      encoded3,
+      encoded4;
+    if (base64[base64.length - 1] === '=') {
+      bufferLength--;
+      if (base64[base64.length - 2] === '=') {
+        bufferLength--;
+      }
+    }
+    var arraybuffer = new ArrayBuffer(bufferLength),
+      bytes = new Uint8Array(arraybuffer);
+    for (i = 0; i < len; i += 4) {
+      encoded1 = lookup$1[base64.charCodeAt(i)];
+      encoded2 = lookup$1[base64.charCodeAt(i + 1)];
+      encoded3 = lookup$1[base64.charCodeAt(i + 2)];
+      encoded4 = lookup$1[base64.charCodeAt(i + 3)];
+      bytes[p++] = encoded1 << 2 | encoded2 >> 4;
+      bytes[p++] = (encoded2 & 15) << 4 | encoded3 >> 2;
+      bytes[p++] = (encoded3 & 3) << 6 | encoded4 & 63;
+    }
+    return arraybuffer;
+  };
+
+  var withNativeArrayBuffer$1 = typeof ArrayBuffer === "function";
+  var decodePacket = function decodePacket(encodedPacket, binaryType) {
+    if (typeof encodedPacket !== "string") {
+      return {
+        type: "message",
+        data: mapBinary(encodedPacket, binaryType)
+      };
+    }
+    var type = encodedPacket.charAt(0);
+    if (type === "b") {
+      return {
+        type: "message",
+        data: decodeBase64Packet(encodedPacket.substring(1), binaryType)
+      };
+    }
+    var packetType = PACKET_TYPES_REVERSE[type];
+    if (!packetType) {
+      return ERROR_PACKET;
+    }
+    return encodedPacket.length > 1 ? {
+      type: PACKET_TYPES_REVERSE[type],
+      data: encodedPacket.substring(1)
+    } : {
+      type: PACKET_TYPES_REVERSE[type]
+    };
+  };
+  var decodeBase64Packet = function decodeBase64Packet(data, binaryType) {
+    if (withNativeArrayBuffer$1) {
+      var decoded = decode$1(data);
+      return mapBinary(decoded, binaryType);
+    } else {
+      return {
+        base64: true,
+        data: data
+      }; // fallback for old browsers
+    }
+  };
+
+  var mapBinary = function mapBinary(data, binaryType) {
+    switch (binaryType) {
+      case "blob":
+        if (data instanceof Blob) {
+          // from WebSocket + binaryType "blob"
+          return data;
+        } else {
+          // from HTTP long-polling or WebTransport
+          return new Blob([data]);
+        }
+      case "arraybuffer":
+      default:
+        if (data instanceof ArrayBuffer) {
+          // from HTTP long-polling (base64) or WebSocket + binaryType "arraybuffer"
+          return data;
+        } else {
+          // from WebTransport (Uint8Array)
+          return data.buffer;
+        }
+    }
+  };
+
+  var SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
+  var encodePayload = function encodePayload(packets, callback) {
+    // some packets may be added to the array while encoding, so the initial length must be saved
+    var length = packets.length;
+    var encodedPackets = new Array(length);
+    var count = 0;
+    packets.forEach(function (packet, i) {
+      // force base64 encoding for binary packets
+      encodePacket(packet, false, function (encodedPacket) {
+        encodedPackets[i] = encodedPacket;
+        if (++count === length) {
+          callback(encodedPackets.join(SEPARATOR));
+        }
+      });
+    });
+  };
+  var decodePayload = function decodePayload(encodedPayload, binaryType) {
+    var encodedPackets = encodedPayload.split(SEPARATOR);
+    var packets = [];
+    for (var i = 0; i < encodedPackets.length; i++) {
+      var decodedPacket = decodePacket(encodedPackets[i], binaryType);
+      packets.push(decodedPacket);
+      if (decodedPacket.type === "error") {
+        break;
+      }
+    }
+    return packets;
+  };
+  function createPacketEncoderStream() {
+    return new TransformStream({
+      transform: function transform(packet, controller) {
+        encodePacketToBinary(packet, function (encodedPacket) {
+          var payloadLength = encodedPacket.length;
+          var header;
+          // inspired by the WebSocket format: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#decoding_payload_length
+          if (payloadLength < 126) {
+            header = new Uint8Array(1);
+            new DataView(header.buffer).setUint8(0, payloadLength);
+          } else if (payloadLength < 65536) {
+            header = new Uint8Array(3);
+            var view = new DataView(header.buffer);
+            view.setUint8(0, 126);
+            view.setUint16(1, payloadLength);
+          } else {
+            header = new Uint8Array(9);
+            var _view = new DataView(header.buffer);
+            _view.setUint8(0, 127);
+            _view.setBigUint64(1, BigInt(payloadLength));
+          }
+          // first bit indicates whether the payload is plain text (0) or binary (1)
+          if (packet.data && typeof packet.data !== "string") {
+            header[0] |= 0x80;
+          }
+          controller.enqueue(header);
+          controller.enqueue(encodedPacket);
+        });
+      }
+    });
+  }
+  var TEXT_DECODER;
+  function totalLength(chunks) {
+    return chunks.reduce(function (acc, chunk) {
+      return acc + chunk.length;
+    }, 0);
+  }
+  function concatChunks(chunks, size) {
+    if (chunks[0].length === size) {
+      return chunks.shift();
+    }
+    var buffer = new Uint8Array(size);
+    var j = 0;
+    for (var i = 0; i < size; i++) {
+      buffer[i] = chunks[0][j++];
+      if (j === chunks[0].length) {
+        chunks.shift();
+        j = 0;
+      }
+    }
+    if (chunks.length && j < chunks[0].length) {
+      chunks[0] = chunks[0].slice(j);
+    }
+    return buffer;
+  }
+  function createPacketDecoderStream(maxPayload, binaryType) {
+    if (!TEXT_DECODER) {
+      TEXT_DECODER = new TextDecoder();
+    }
+    var chunks = [];
+    var state = 0 /* READ_HEADER */;
+    var expectedLength = -1;
+    var isBinary = false;
+    return new TransformStream({
+      transform: function transform(chunk, controller) {
+        chunks.push(chunk);
+        while (true) {
+          if (state === 0 /* READ_HEADER */) {
+            if (totalLength(chunks) < 1) {
+              break;
+            }
+            var header = concatChunks(chunks, 1);
+            isBinary = (header[0] & 0x80) === 0x80;
+            expectedLength = header[0] & 0x7f;
+            if (expectedLength < 126) {
+              state = 3 /* READ_PAYLOAD */;
+            } else if (expectedLength === 126) {
+              state = 1 /* READ_EXTENDED_LENGTH_16 */;
+            } else {
+              state = 2 /* READ_EXTENDED_LENGTH_64 */;
+            }
+          } else if (state === 1 /* READ_EXTENDED_LENGTH_16 */) {
+            if (totalLength(chunks) < 2) {
+              break;
+            }
+            var headerArray = concatChunks(chunks, 2);
+            expectedLength = new DataView(headerArray.buffer, headerArray.byteOffset, headerArray.length).getUint16(0);
+            state = 3 /* READ_PAYLOAD */;
+          } else if (state === 2 /* READ_EXTENDED_LENGTH_64 */) {
+            if (totalLength(chunks) < 8) {
+              break;
+            }
+            var _headerArray = concatChunks(chunks, 8);
+            var view = new DataView(_headerArray.buffer, _headerArray.byteOffset, _headerArray.length);
+            var n = view.getUint32(0);
+            if (n > Math.pow(2, 53 - 32) - 1) {
+              // the maximum safe integer in JavaScript is 2^53 - 1
+              controller.enqueue(ERROR_PACKET);
+              break;
+            }
+            expectedLength = n * Math.pow(2, 32) + view.getUint32(4);
+            state = 3 /* READ_PAYLOAD */;
+          } else {
+            if (totalLength(chunks) < expectedLength) {
+              break;
+            }
+            var data = concatChunks(chunks, expectedLength);
+            controller.enqueue(decodePacket(isBinary ? data : TEXT_DECODER.decode(data), binaryType));
+            state = 0 /* READ_HEADER */;
+          }
+
+          if (expectedLength === 0 || expectedLength > maxPayload) {
+            controller.enqueue(ERROR_PACKET);
+            break;
+          }
+        }
+      }
+    });
+  }
+  var protocol$1 = 4;
+
+  /**
+   * Initialize a new `Emitter`.
+   *
+   * @api public
+   */
+
+  function Emitter(obj) {
+    if (obj) return mixin(obj);
+  }
+
+  /**
+   * Mixin the emitter properties.
+   *
+   * @param {Object} obj
+   * @return {Object}
+   * @api private
+   */
+
+  function mixin(obj) {
+    for (var key in Emitter.prototype) {
+      obj[key] = Emitter.prototype[key];
+    }
+    return obj;
+  }
+
+  /**
+   * Listen on the given `event` with `fn`.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.on = Emitter.prototype.addEventListener = function (event, fn) {
+    this._callbacks = this._callbacks || {};
+    (this._callbacks['$' + event] = this._callbacks['$' + event] || []).push(fn);
+    return this;
+  };
+
+  /**
+   * Adds an `event` listener that will be invoked a single
+   * time then automatically removed.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.once = function (event, fn) {
+    function on() {
+      this.off(event, on);
+      fn.apply(this, arguments);
+    }
+    on.fn = fn;
+    this.on(event, on);
+    return this;
+  };
+
+  /**
+   * Remove the given callback for `event` or all
+   * registered callbacks.
+   *
+   * @param {String} event
+   * @param {Function} fn
+   * @return {Emitter}
+   * @api public
+   */
+
+  Emitter.prototype.off = Emitter.prototype.removeListener = Emitter.prototype.removeAllListeners = Emitter.prototype.removeEventListener = function (event, fn) {
+    this._callbacks = this._callbacks || {};
+
+    // all
+    if (0 == arguments.length) {
+      this._callbacks = {};
+      return this;
+    }
+
+    // specific event
+    var callbacks = this._callbacks['$' + event];
+    if (!callbacks) return this;
+
+    // remove all handlers
+    if (1 == arguments.length) {
+      delete this._callbacks['$' + event];
+      return this;
+    }
+
+    // remove specific handler
+    var cb;
+    for (var i = 0; i < callbacks.length; i++) {
+      cb = callbacks[i];
+      if (cb === fn || cb.fn === fn) {
+        callbacks.splice(i, 1);
+        break;
+      }
+    }
+
+    // Remove event specific arrays for event types that no
+    // one is subscribed for to avoid memory leak.
+    if (callbacks.length === 0) {
+      delete this._callbacks['$' + event];
+    }
+    return this;
+  };
+
+  /**
+   * Emit `event` with the given args.
+   *
+   * @param {String} event
+   * @param {Mixed} ...
+   * @return {Emitter}
+   */
+
+  Emitter.prototype.emit = function (event) {
+    this._callbacks = this._callbacks || {};
+    var args = new Array(arguments.length - 1),
+      callbacks = this._callbacks['$' + event];
+    for (var i = 1; i < arguments.length; i++) {
+      args[i - 1] = arguments[i];
+    }
+    if (callbacks) {
+      callbacks = callbacks.slice(0);
+      for (var i = 0, len = callbacks.length; i < len; ++i) {
+        callbacks[i].apply(this, args);
+      }
+    }
+    return this;
+  };
+
+  // alias used for reserved events (protected method)
+  Emitter.prototype.emitReserved = Emitter.prototype.emit;
+
+  /**
+   * Return array of callbacks for `event`.
+   *
+   * @param {String} event
+   * @return {Array}
+   * @api public
+   */
+
+  Emitter.prototype.listeners = function (event) {
+    this._callbacks = this._callbacks || {};
+    return this._callbacks['$' + event] || [];
+  };
+
+  /**
+   * Check if this emitter has `event` handlers.
+   *
+   * @param {String} event
+   * @return {Boolean}
+   * @api public
+   */
+
+  Emitter.prototype.hasListeners = function (event) {
+    return !!this.listeners(event).length;
+  };
+
+  var globalThisShim = function () {
+    if (typeof self !== "undefined") {
+      return self;
+    } else if (typeof window !== "undefined") {
+      return window;
+    } else {
+      return Function("return this")();
+    }
+  }();
+
+  function pick(obj) {
+    for (var _len = arguments.length, attr = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+      attr[_key - 1] = arguments[_key];
+    }
+    return attr.reduce(function (acc, k) {
+      if (obj.hasOwnProperty(k)) {
+        acc[k] = obj[k];
+      }
+      return acc;
+    }, {});
+  }
+  // Keep a reference to the real timeout functions so they can be used when overridden
+  var NATIVE_SET_TIMEOUT = globalThisShim.setTimeout;
+  var NATIVE_CLEAR_TIMEOUT = globalThisShim.clearTimeout;
+  function installTimerFunctions(obj, opts) {
+    if (opts.useNativeTimers) {
+      obj.setTimeoutFn = NATIVE_SET_TIMEOUT.bind(globalThisShim);
+      obj.clearTimeoutFn = NATIVE_CLEAR_TIMEOUT.bind(globalThisShim);
+    } else {
+      obj.setTimeoutFn = globalThisShim.setTimeout.bind(globalThisShim);
+      obj.clearTimeoutFn = globalThisShim.clearTimeout.bind(globalThisShim);
+    }
+  }
+  // base64 encoded buffers are about 33% bigger (https://en.wikipedia.org/wiki/Base64)
+  var BASE64_OVERHEAD = 1.33;
+  // we could also have used `new Blob([obj]).size`, but it isn't supported in IE9
+  function byteLength(obj) {
+    if (typeof obj === "string") {
+      return utf8Length(obj);
+    }
+    // arraybuffer or blob
+    return Math.ceil((obj.byteLength || obj.size) * BASE64_OVERHEAD);
+  }
+  function utf8Length(str) {
+    var c = 0,
+      length = 0;
+    for (var i = 0, l = str.length; i < l; i++) {
+      c = str.charCodeAt(i);
+      if (c < 0x80) {
+        length += 1;
+      } else if (c < 0x800) {
+        length += 2;
+      } else if (c < 0xd800 || c >= 0xe000) {
+        length += 3;
+      } else {
+        i++;
+        length += 4;
+      }
+    }
+    return length;
+  }
+
+  // imported from https://github.com/galkn/querystring
+  /**
+   * Compiles a querystring
+   * Returns string representation of the object
+   *
+   * @param {Object}
+   * @api private
+   */
+  function encode$1(obj) {
+    var str = '';
+    for (var i in obj) {
+      if (obj.hasOwnProperty(i)) {
+        if (str.length) str += '&';
+        str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);
+      }
+    }
+    return str;
+  }
+  /**
+   * Parses a simple querystring into an object
+   *
+   * @param {String} qs
+   * @api private
+   */
+  function decode(qs) {
+    var qry = {};
+    var pairs = qs.split('&');
+    for (var i = 0, l = pairs.length; i < l; i++) {
+      var pair = pairs[i].split('=');
+      qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
+    }
+    return qry;
+  }
+
+  var TransportError = /*#__PURE__*/function (_Error) {
+    _inherits(TransportError, _Error);
+    var _super = _createSuper(TransportError);
+    function TransportError(reason, description, context) {
+      var _this;
+      _classCallCheck(this, TransportError);
+      _this = _super.call(this, reason);
+      _this.description = description;
+      _this.context = context;
+      _this.type = "TransportError";
+      return _this;
+    }
+    return _createClass(TransportError);
+  }( /*#__PURE__*/_wrapNativeSuper(Error));
+  var Transport = /*#__PURE__*/function (_Emitter) {
+    _inherits(Transport, _Emitter);
+    var _super2 = _createSuper(Transport);
+    /**
+     * Transport abstract constructor.
+     *
+     * @param {Object} opts - options
+     * @protected
+     */
+    function Transport(opts) {
+      var _this2;
+      _classCallCheck(this, Transport);
+      _this2 = _super2.call(this);
+      _this2.writable = false;
+      installTimerFunctions(_assertThisInitialized(_this2), opts);
+      _this2.opts = opts;
+      _this2.query = opts.query;
+      _this2.socket = opts.socket;
+      return _this2;
+    }
+    /**
+     * Emits an error.
+     *
+     * @param {String} reason
+     * @param description
+     * @param context - the error context
+     * @return {Transport} for chaining
+     * @protected
+     */
+    _createClass(Transport, [{
+      key: "onError",
+      value: function onError(reason, description, context) {
+        _get(_getPrototypeOf(Transport.prototype), "emitReserved", this).call(this, "error", new TransportError(reason, description, context));
+        return this;
+      }
+      /**
+       * Opens the transport.
+       */
+    }, {
+      key: "open",
+      value: function open() {
+        this.readyState = "opening";
+        this.doOpen();
+        return this;
+      }
+      /**
+       * Closes the transport.
+       */
+    }, {
+      key: "close",
+      value: function close() {
+        if (this.readyState === "opening" || this.readyState === "open") {
+          this.doClose();
+          this.onClose();
+        }
+        return this;
+      }
+      /**
+       * Sends multiple packets.
+       *
+       * @param {Array} packets
+       */
+    }, {
+      key: "send",
+      value: function send(packets) {
+        if (this.readyState === "open") {
+          this.write(packets);
+        }
+      }
+      /**
+       * Called upon open
+       *
+       * @protected
+       */
+    }, {
+      key: "onOpen",
+      value: function onOpen() {
+        this.readyState = "open";
+        this.writable = true;
+        _get(_getPrototypeOf(Transport.prototype), "emitReserved", this).call(this, "open");
+      }
+      /**
+       * Called with data.
+       *
+       * @param {String} data
+       * @protected
+       */
+    }, {
+      key: "onData",
+      value: function onData(data) {
+        var packet = decodePacket(data, this.socket.binaryType);
+        this.onPacket(packet);
+      }
+      /**
+       * Called with a decoded packet.
+       *
+       * @protected
+       */
+    }, {
+      key: "onPacket",
+      value: function onPacket(packet) {
+        _get(_getPrototypeOf(Transport.prototype), "emitReserved", this).call(this, "packet", packet);
+      }
+      /**
+       * Called upon close.
+       *
+       * @protected
+       */
+    }, {
+      key: "onClose",
+      value: function onClose(details) {
+        this.readyState = "closed";
+        _get(_getPrototypeOf(Transport.prototype), "emitReserved", this).call(this, "close", details);
+      }
+      /**
+       * Pauses the transport, in order not to lose packets during an upgrade.
+       *
+       * @param onPause
+       */
+    }, {
+      key: "pause",
+      value: function pause(onPause) {}
+    }, {
+      key: "createUri",
+      value: function createUri(schema) {
+        var query = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+        return schema + "://" + this._hostname() + this._port() + this.opts.path + this._query(query);
+      }
+    }, {
+      key: "_hostname",
+      value: function _hostname() {
+        var hostname = this.opts.hostname;
+        return hostname.indexOf(":") === -1 ? hostname : "[" + hostname + "]";
+      }
+    }, {
+      key: "_port",
+      value: function _port() {
+        if (this.opts.port && (this.opts.secure && Number(this.opts.port !== 443) || !this.opts.secure && Number(this.opts.port) !== 80)) {
+          return ":" + this.opts.port;
+        } else {
+          return "";
+        }
+      }
+    }, {
+      key: "_query",
+      value: function _query(query) {
+        var encodedQuery = encode$1(query);
+        return encodedQuery.length ? "?" + encodedQuery : "";
+      }
+    }]);
+    return Transport;
+  }(Emitter);
+
+  // imported from https://github.com/unshiftio/yeast
+
+  var alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_'.split(''),
+    length = 64,
+    map = {};
+  var seed = 0,
+    i = 0,
+    prev;
+  /**
+   * Return a string representing the specified number.
+   *
+   * @param {Number} num The number to convert.
+   * @returns {String} The string representation of the number.
+   * @api public
+   */
+  function encode(num) {
+    var encoded = '';
+    do {
+      encoded = alphabet[num % length] + encoded;
+      num = Math.floor(num / length);
+    } while (num > 0);
+    return encoded;
+  }
+  /**
+   * Yeast: A tiny growing id generator.
+   *
+   * @returns {String} A unique id.
+   * @api public
+   */
+  function yeast() {
+    var now = encode(+new Date());
+    if (now !== prev) return seed = 0, prev = now;
+    return now + '.' + encode(seed++);
+  }
+  //
+  // Map each character to its index.
+  //
+  for (; i < length; i++) map[alphabet[i]] = i;
+
+  // imported from https://github.com/component/has-cors
+  var value = false;
+  try {
+    value = typeof XMLHttpRequest !== 'undefined' && 'withCredentials' in new XMLHttpRequest();
+  } catch (err) {
+    // if XMLHttp support is disabled in IE then it will throw
+    // when trying to create
+  }
+  var hasCORS = value;
+
+  // browser shim for xmlhttprequest module
+  function XHR(opts) {
+    var xdomain = opts.xdomain;
+    // XMLHttpRequest can be disabled on IE
+    try {
+      if ("undefined" !== typeof XMLHttpRequest && (!xdomain || hasCORS)) {
+        return new XMLHttpRequest();
+      }
+    } catch (e) {}
+    if (!xdomain) {
+      try {
+        return new globalThisShim[["Active"].concat("Object").join("X")]("Microsoft.XMLHTTP");
+      } catch (e) {}
+    }
+  }
+  function createCookieJar() {}
+
+  function empty() {}
+  var hasXHR2 = function () {
+    var xhr = new XHR({
+      xdomain: false
+    });
+    return null != xhr.responseType;
+  }();
+  var Polling = /*#__PURE__*/function (_Transport) {
+    _inherits(Polling, _Transport);
+    var _super = _createSuper(Polling);
+    /**
+     * XHR Polling constructor.
+     *
+     * @param {Object} opts
+     * @package
+     */
+    function Polling(opts) {
+      var _this;
+      _classCallCheck(this, Polling);
+      _this = _super.call(this, opts);
+      _this.polling = false;
+      if (typeof location !== "undefined") {
+        var isSSL = "https:" === location.protocol;
+        var port = location.port;
+        // some user agents have empty `location.port`
+        if (!port) {
+          port = isSSL ? "443" : "80";
+        }
+        _this.xd = typeof location !== "undefined" && opts.hostname !== location.hostname || port !== opts.port;
+      }
+      /**
+       * XHR supports binary
+       */
+      var forceBase64 = opts && opts.forceBase64;
+      _this.supportsBinary = hasXHR2 && !forceBase64;
+      if (_this.opts.withCredentials) {
+        _this.cookieJar = createCookieJar();
+      }
+      return _this;
+    }
+    _createClass(Polling, [{
+      key: "name",
+      get: function get() {
+        return "polling";
+      }
+      /**
+       * Opens the socket (triggers polling). We write a PING message to determine
+       * when the transport is open.
+       *
+       * @protected
+       */
+    }, {
+      key: "doOpen",
+      value: function doOpen() {
+        this.poll();
+      }
+      /**
+       * Pauses polling.
+       *
+       * @param {Function} onPause - callback upon buffers are flushed and transport is paused
+       * @package
+       */
+    }, {
+      key: "pause",
+      value: function pause(onPause) {
+        var _this2 = this;
+        this.readyState = "pausing";
+        var pause = function pause() {
+          _this2.readyState = "paused";
+          onPause();
+        };
+        if (this.polling || !this.writable) {
+          var total = 0;
+          if (this.polling) {
+            total++;
+            this.once("pollComplete", function () {
+              --total || pause();
+            });
+          }
+          if (!this.writable) {
+            total++;
+            this.once("drain", function () {
+              --total || pause();
+            });
+          }
+        } else {
+          pause();
+        }
+      }
+      /**
+       * Starts polling cycle.
+       *
+       * @private
+       */
+    }, {
+      key: "poll",
+      value: function poll() {
+        this.polling = true;
+        this.doPoll();
+        this.emitReserved("poll");
+      }
+      /**
+       * Overloads onData to detect payloads.
+       *
+       * @protected
+       */
+    }, {
+      key: "onData",
+      value: function onData(data) {
+        var _this3 = this;
+        var callback = function callback(packet) {
+          // if its the first message we consider the transport open
+          if ("opening" === _this3.readyState && packet.type === "open") {
+            _this3.onOpen();
+          }
+          // if its a close packet, we close the ongoing requests
+          if ("close" === packet.type) {
+            _this3.onClose({
+              description: "transport closed by the server"
+            });
+            return false;
+          }
+          // otherwise bypass onData and handle the message
+          _this3.onPacket(packet);
+        };
+        // decode payload
+        decodePayload(data, this.socket.binaryType).forEach(callback);
+        // if an event did not trigger closing
+        if ("closed" !== this.readyState) {
+          // if we got data we're not polling
+          this.polling = false;
+          this.emitReserved("pollComplete");
+          if ("open" === this.readyState) {
+            this.poll();
+          }
+        }
+      }
+      /**
+       * For polling, send a close packet.
+       *
+       * @protected
+       */
+    }, {
+      key: "doClose",
+      value: function doClose() {
+        var _this4 = this;
+        var close = function close() {
+          _this4.write([{
+            type: "close"
+          }]);
+        };
+        if ("open" === this.readyState) {
+          close();
+        } else {
+          // in case we're trying to close while
+          // handshaking is in progress (GH-164)
+          this.once("open", close);
+        }
+      }
+      /**
+       * Writes a packets payload.
+       *
+       * @param {Array} packets - data packets
+       * @protected
+       */
+    }, {
+      key: "write",
+      value: function write(packets) {
+        var _this5 = this;
+        this.writable = false;
+        encodePayload(packets, function (data) {
+          _this5.doWrite(data, function () {
+            _this5.writable = true;
+            _this5.emitReserved("drain");
+          });
+        });
+      }
+      /**
+       * Generates uri for connection.
+       *
+       * @private
+       */
+    }, {
+      key: "uri",
+      value: function uri() {
+        var schema = this.opts.secure ? "https" : "http";
+        var query = this.query || {};
+        // cache busting is forced
+        if (false !== this.opts.timestampRequests) {
+          query[this.opts.timestampParam] = yeast();
+        }
+        if (!this.supportsBinary && !query.sid) {
+          query.b64 = 1;
+        }
+        return this.createUri(schema, query);
+      }
+      /**
+       * Creates a request.
+       *
+       * @param {String} method
+       * @private
+       */
+    }, {
+      key: "request",
+      value: function request() {
+        var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+        _extends(opts, {
+          xd: this.xd,
+          cookieJar: this.cookieJar
+        }, this.opts);
+        return new Request(this.uri(), opts);
+      }
+      /**
+       * Sends data.
+       *
+       * @param {String} data to send.
+       * @param {Function} called upon flush.
+       * @private
+       */
+    }, {
+      key: "doWrite",
+      value: function doWrite(data, fn) {
+        var _this6 = this;
+        var req = this.request({
+          method: "POST",
+          data: data
+        });
+        req.on("success", fn);
+        req.on("error", function (xhrStatus, context) {
+          _this6.onError("xhr post error", xhrStatus, context);
+        });
+      }
+      /**
+       * Starts a poll cycle.
+       *
+       * @private
+       */
+    }, {
+      key: "doPoll",
+      value: function doPoll() {
+        var _this7 = this;
+        var req = this.request();
+        req.on("data", this.onData.bind(this));
+        req.on("error", function (xhrStatus, context) {
+          _this7.onError("xhr poll error", xhrStatus, context);
+        });
+        this.pollXhr = req;
+      }
+    }]);
+    return Polling;
+  }(Transport);
+  var Request = /*#__PURE__*/function (_Emitter) {
+    _inherits(Request, _Emitter);
+    var _super2 = _createSuper(Request);
+    /**
+     * Request constructor
+     *
+     * @param {Object} options
+     * @package
+     */
+    function Request(uri, opts) {
+      var _this8;
+      _classCallCheck(this, Request);
+      _this8 = _super2.call(this);
+      installTimerFunctions(_assertThisInitialized(_this8), opts);
+      _this8.opts = opts;
+      _this8.method = opts.method || "GET";
+      _this8.uri = uri;
+      _this8.data = undefined !== opts.data ? opts.data : null;
+      _this8.create();
+      return _this8;
+    }
+    /**
+     * Creates the XHR object and sends the request.
+     *
+     * @private
+     */
+    _createClass(Request, [{
+      key: "create",
+      value: function create() {
+        var _this9 = this;
+        var _a;
+        var opts = pick(this.opts, "agent", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "autoUnref");
+        opts.xdomain = !!this.opts.xd;
+        var xhr = this.xhr = new XHR(opts);
+        try {
+          xhr.open(this.method, this.uri, true);
+          try {
+            if (this.opts.extraHeaders) {
+              xhr.setDisableHeaderCheck && xhr.setDisableHeaderCheck(true);
+              for (var i in this.opts.extraHeaders) {
+                if (this.opts.extraHeaders.hasOwnProperty(i)) {
+                  xhr.setRequestHeader(i, this.opts.extraHeaders[i]);
+                }
+              }
+            }
+          } catch (e) {}
+          if ("POST" === this.method) {
+            try {
+              xhr.setRequestHeader("Content-type", "text/plain;charset=UTF-8");
+            } catch (e) {}
+          }
+          try {
+            xhr.setRequestHeader("Accept", "*/*");
+          } catch (e) {}
+          (_a = this.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.addCookies(xhr);
+          // ie6 check
+          if ("withCredentials" in xhr) {
+            xhr.withCredentials = this.opts.withCredentials;
+          }
+          if (this.opts.requestTimeout) {
+            xhr.timeout = this.opts.requestTimeout;
+          }
+          xhr.onreadystatechange = function () {
+            var _a;
+            if (xhr.readyState === 3) {
+              (_a = _this9.opts.cookieJar) === null || _a === void 0 ? void 0 : _a.parseCookies(xhr);
+            }
+            if (4 !== xhr.readyState) return;
+            if (200 === xhr.status || 1223 === xhr.status) {
+              _this9.onLoad();
+            } else {
+              // make sure the `error` event handler that's user-set
+              // does not throw in the same tick and gets caught here
+              _this9.setTimeoutFn(function () {
+                _this9.onError(typeof xhr.status === "number" ? xhr.status : 0);
+              }, 0);
+            }
+          };
+          xhr.send(this.data);
+        } catch (e) {
+          // Need to defer since .create() is called directly from the constructor
+          // and thus the 'error' event can only be only bound *after* this exception
+          // occurs.  Therefore, also, we cannot throw here at all.
+          this.setTimeoutFn(function () {
+            _this9.onError(e);
+          }, 0);
+          return;
+        }
+        if (typeof document !== "undefined") {
+          this.index = Request.requestsCount++;
+          Request.requests[this.index] = this;
+        }
+      }
+      /**
+       * Called upon error.
+       *
+       * @private
+       */
+    }, {
+      key: "onError",
+      value: function onError(err) {
+        this.emitReserved("error", err, this.xhr);
+        this.cleanup(true);
+      }
+      /**
+       * Cleans up house.
+       *
+       * @private
+       */
+    }, {
+      key: "cleanup",
+      value: function cleanup(fromError) {
+        if ("undefined" === typeof this.xhr || null === this.xhr) {
+          return;
+        }
+        this.xhr.onreadystatechange = empty;
+        if (fromError) {
+          try {
+            this.xhr.abort();
+          } catch (e) {}
+        }
+        if (typeof document !== "undefined") {
+          delete Request.requests[this.index];
+        }
+        this.xhr = null;
+      }
+      /**
+       * Called upon load.
+       *
+       * @private
+       */
+    }, {
+      key: "onLoad",
+      value: function onLoad() {
+        var data = this.xhr.responseText;
+        if (data !== null) {
+          this.emitReserved("data", data);
+          this.emitReserved("success");
+          this.cleanup();
+        }
+      }
+      /**
+       * Aborts the request.
+       *
+       * @package
+       */
+    }, {
+      key: "abort",
+      value: function abort() {
+        this.cleanup();
+      }
+    }]);
+    return Request;
+  }(Emitter);
+  Request.requestsCount = 0;
+  Request.requests = {};
+  /**
+   * Aborts pending requests when unloading the window. This is needed to prevent
+   * memory leaks (e.g. when using IE) and to ensure that no spurious error is
+   * emitted.
+   */
+  if (typeof document !== "undefined") {
+    // @ts-ignore
+    if (typeof attachEvent === "function") {
+      // @ts-ignore
+      attachEvent("onunload", unloadHandler);
+    } else if (typeof addEventListener === "function") {
+      var terminationEvent = "onpagehide" in globalThisShim ? "pagehide" : "unload";
+      addEventListener(terminationEvent, unloadHandler, false);
+    }
+  }
+  function unloadHandler() {
+    for (var i in Request.requests) {
+      if (Request.requests.hasOwnProperty(i)) {
+        Request.requests[i].abort();
+      }
+    }
+  }
+
+  var nextTick = function () {
+    var isPromiseAvailable = typeof Promise === "function" && typeof Promise.resolve === "function";
+    if (isPromiseAvailable) {
+      return function (cb) {
+        return Promise.resolve().then(cb);
+      };
+    } else {
+      return function (cb, setTimeoutFn) {
+        return setTimeoutFn(cb, 0);
+      };
+    }
+  }();
+  var WebSocket = globalThisShim.WebSocket || globalThisShim.MozWebSocket;
+  var usingBrowserWebSocket = true;
+  var defaultBinaryType = "arraybuffer";
+
+  // detect ReactNative environment
+  var isReactNative = typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product.toLowerCase() === "reactnative";
+  var WS = /*#__PURE__*/function (_Transport) {
+    _inherits(WS, _Transport);
+    var _super = _createSuper(WS);
+    /**
+     * WebSocket transport constructor.
+     *
+     * @param {Object} opts - connection options
+     * @protected
+     */
+    function WS(opts) {
+      var _this;
+      _classCallCheck(this, WS);
+      _this = _super.call(this, opts);
+      _this.supportsBinary = !opts.forceBase64;
+      return _this;
+    }
+    _createClass(WS, [{
+      key: "name",
+      get: function get() {
+        return "websocket";
+      }
+    }, {
+      key: "doOpen",
+      value: function doOpen() {
+        if (!this.check()) {
+          // let probe timeout
+          return;
+        }
+        var uri = this.uri();
+        var protocols = this.opts.protocols;
+        // React Native only supports the 'headers' option, and will print a warning if anything else is passed
+        var opts = isReactNative ? {} : pick(this.opts, "agent", "perMessageDeflate", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "localAddress", "protocolVersion", "origin", "maxPayload", "family", "checkServerIdentity");
+        if (this.opts.extraHeaders) {
+          opts.headers = this.opts.extraHeaders;
+        }
+        try {
+          this.ws = usingBrowserWebSocket && !isReactNative ? protocols ? new WebSocket(uri, protocols) : new WebSocket(uri) : new WebSocket(uri, protocols, opts);
+        } catch (err) {
+          return this.emitReserved("error", err);
+        }
+        this.ws.binaryType = this.socket.binaryType;
+        this.addEventListeners();
+      }
+      /**
+       * Adds event listeners to the socket
+       *
+       * @private
+       */
+    }, {
+      key: "addEventListeners",
+      value: function addEventListeners() {
+        var _this2 = this;
+        this.ws.onopen = function () {
+          if (_this2.opts.autoUnref) {
+            _this2.ws._socket.unref();
+          }
+          _this2.onOpen();
+        };
+        this.ws.onclose = function (closeEvent) {
+          return _this2.onClose({
+            description: "websocket connection closed",
+            context: closeEvent
+          });
+        };
+        this.ws.onmessage = function (ev) {
+          return _this2.onData(ev.data);
+        };
+        this.ws.onerror = function (e) {
+          return _this2.onError("websocket error", e);
+        };
+      }
+    }, {
+      key: "write",
+      value: function write(packets) {
+        var _this3 = this;
+        this.writable = false;
+        // encodePacket efficient as it uses WS framing
+        // no need for encodePayload
+        var _loop = function _loop() {
+          var packet = packets[i];
+          var lastPacket = i === packets.length - 1;
+          encodePacket(packet, _this3.supportsBinary, function (data) {
+            // always create a new object (GH-437)
+            var opts = {};
+            // Sometimes the websocket has already been closed but the browser didn't
+            // have a chance of informing us about it yet, in that case send will
+            // throw an error
+            try {
+              if (usingBrowserWebSocket) {
+                // TypeError is thrown when passing the second argument on Safari
+                _this3.ws.send(data);
+              }
+            } catch (e) {}
+            if (lastPacket) {
+              // fake drain
+              // defer to next tick to allow Socket to clear writeBuffer
+              nextTick(function () {
+                _this3.writable = true;
+                _this3.emitReserved("drain");
+              }, _this3.setTimeoutFn);
+            }
+          });
+        };
+        for (var i = 0; i < packets.length; i++) {
+          _loop();
+        }
+      }
+    }, {
+      key: "doClose",
+      value: function doClose() {
+        if (typeof this.ws !== "undefined") {
+          this.ws.close();
+          this.ws = null;
+        }
+      }
+      /**
+       * Generates uri for connection.
+       *
+       * @private
+       */
+    }, {
+      key: "uri",
+      value: function uri() {
+        var schema = this.opts.secure ? "wss" : "ws";
+        var query = this.query || {};
+        // append timestamp to URI
+        if (this.opts.timestampRequests) {
+          query[this.opts.timestampParam] = yeast();
+        }
+        // communicate binary support capabilities
+        if (!this.supportsBinary) {
+          query.b64 = 1;
+        }
+        return this.createUri(schema, query);
+      }
+      /**
+       * Feature detection for WebSocket.
+       *
+       * @return {Boolean} whether this transport is available.
+       * @private
+       */
+    }, {
+      key: "check",
+      value: function check() {
+        return !!WebSocket;
+      }
+    }]);
+    return WS;
+  }(Transport);
+
+  var WT = /*#__PURE__*/function (_Transport) {
+    _inherits(WT, _Transport);
+    var _super = _createSuper(WT);
+    function WT() {
+      _classCallCheck(this, WT);
+      return _super.apply(this, arguments);
+    }
+    _createClass(WT, [{
+      key: "name",
+      get: function get() {
+        return "webtransport";
+      }
+    }, {
+      key: "doOpen",
+      value: function doOpen() {
+        var _this = this;
+        // @ts-ignore
+        if (typeof WebTransport !== "function") {
+          return;
+        }
+        // @ts-ignore
+        this.transport = new WebTransport(this.createUri("https"), this.opts.transportOptions[this.name]);
+        this.transport.closed.then(function () {
+          _this.onClose();
+        })["catch"](function (err) {
+          _this.onError("webtransport error", err);
+        });
+        // note: we could have used async/await, but that would require some additional polyfills
+        this.transport.ready.then(function () {
+          _this.transport.createBidirectionalStream().then(function (stream) {
+            var decoderStream = createPacketDecoderStream(Number.MAX_SAFE_INTEGER, _this.socket.binaryType);
+            var reader = stream.readable.pipeThrough(decoderStream).getReader();
+            var encoderStream = createPacketEncoderStream();
+            encoderStream.readable.pipeTo(stream.writable);
+            _this.writer = encoderStream.writable.getWriter();
+            var read = function read() {
+              reader.read().then(function (_ref) {
+                var done = _ref.done,
+                  value = _ref.value;
+                if (done) {
+                  return;
+                }
+                _this.onPacket(value);
+                read();
+              })["catch"](function (err) {});
+            };
+            read();
+            var packet = {
+              type: "open"
+            };
+            if (_this.query.sid) {
+              packet.data = "{\"sid\":\"".concat(_this.query.sid, "\"}");
+            }
+            _this.writer.write(packet).then(function () {
+              return _this.onOpen();
+            });
+          });
+        });
+      }
+    }, {
+      key: "write",
+      value: function write(packets) {
+        var _this2 = this;
+        this.writable = false;
+        var _loop = function _loop() {
+          var packet = packets[i];
+          var lastPacket = i === packets.length - 1;
+          _this2.writer.write(packet).then(function () {
+            if (lastPacket) {
+              nextTick(function () {
+                _this2.writable = true;
+                _this2.emitReserved("drain");
+              }, _this2.setTimeoutFn);
+            }
+          });
+        };
+        for (var i = 0; i < packets.length; i++) {
+          _loop();
+        }
+      }
+    }, {
+      key: "doClose",
+      value: function doClose() {
+        var _a;
+        (_a = this.transport) === null || _a === void 0 ? void 0 : _a.close();
+      }
+    }]);
+    return WT;
+  }(Transport);
+
+  var transports = {
+    websocket: WS,
+    webtransport: WT,
+    polling: Polling
+  };
+
+  // imported from https://github.com/galkn/parseuri
+  /**
+   * Parses a URI
+   *
+   * Note: we could also have used the built-in URL object, but it isn't supported on all platforms.
+   *
+   * See:
+   * - https://developer.mozilla.org/en-US/docs/Web/API/URL
+   * - https://caniuse.com/url
+   * - https://www.rfc-editor.org/rfc/rfc3986#appendix-B
+   *
+   * History of the parse() method:
+   * - first commit: https://github.com/socketio/socket.io-client/commit/4ee1d5d94b3906a9c052b459f1a818b15f38f91c
+   * - export into its own module: https://github.com/socketio/engine.io-client/commit/de2c561e4564efeb78f1bdb1ba39ef81b2822cb3
+   * - reimport: https://github.com/socketio/engine.io-client/commit/df32277c3f6d622eec5ed09f493cae3f3391d242
+   *
+   * @author Steven Levithan <stevenlevithan.com> (MIT license)
+   * @api private
+   */
+  var re = /^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
+  var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'];
+  function parse(str) {
+    var src = str,
+      b = str.indexOf('['),
+      e = str.indexOf(']');
+    if (b != -1 && e != -1) {
+      str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);
+    }
+    var m = re.exec(str || ''),
+      uri = {},
+      i = 14;
+    while (i--) {
+      uri[parts[i]] = m[i] || '';
+    }
+    if (b != -1 && e != -1) {
+      uri.source = src;
+      uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');
+      uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');
+      uri.ipv6uri = true;
+    }
+    uri.pathNames = pathNames(uri, uri['path']);
+    uri.queryKey = queryKey(uri, uri['query']);
+    return uri;
+  }
+  function pathNames(obj, path) {
+    var regx = /\/{2,9}/g,
+      names = path.replace(regx, "/").split("/");
+    if (path.slice(0, 1) == '/' || path.length === 0) {
+      names.splice(0, 1);
+    }
+    if (path.slice(-1) == '/') {
+      names.splice(names.length - 1, 1);
+    }
+    return names;
+  }
+  function queryKey(uri, query) {
+    var data = {};
+    query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {
+      if ($1) {
+        data[$1] = $2;
+      }
+    });
+    return data;
+  }
+
+  var Socket$1 = /*#__PURE__*/function (_Emitter) {
+    _inherits(Socket, _Emitter);
+    var _super = _createSuper(Socket);
+    /**
+     * Socket constructor.
+     *
+     * @param {String|Object} uri - uri or options
+     * @param {Object} opts - options
+     */
+    function Socket(uri) {
+      var _this;
+      var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+      _classCallCheck(this, Socket);
+      _this = _super.call(this);
+      _this.binaryType = defaultBinaryType;
+      _this.writeBuffer = [];
+      if (uri && "object" === _typeof(uri)) {
+        opts = uri;
+        uri = null;
+      }
+      if (uri) {
+        uri = parse(uri);
+        opts.hostname = uri.host;
+        opts.secure = uri.protocol === "https" || uri.protocol === "wss";
+        opts.port = uri.port;
+        if (uri.query) opts.query = uri.query;
+      } else if (opts.host) {
+        opts.hostname = parse(opts.host).host;
+      }
+      installTimerFunctions(_assertThisInitialized(_this), opts);
+      _this.secure = null != opts.secure ? opts.secure : typeof location !== "undefined" && "https:" === location.protocol;
+      if (opts.hostname && !opts.port) {
+        // if no port is specified manually, use the protocol default
+        opts.port = _this.secure ? "443" : "80";
+      }
+      _this.hostname = opts.hostname || (typeof location !== "undefined" ? location.hostname : "localhost");
+      _this.port = opts.port || (typeof location !== "undefined" && location.port ? location.port : _this.secure ? "443" : "80");
+      _this.transports = opts.transports || ["polling", "websocket", "webtransport"];
+      _this.writeBuffer = [];
+      _this.prevBufferLen = 0;
+      _this.opts = _extends({
+        path: "/engine.io",
+        agent: false,
+        withCredentials: false,
+        upgrade: true,
+        timestampParam: "t",
+        rememberUpgrade: false,
+        addTrailingSlash: true,
+        rejectUnauthorized: true,
+        perMessageDeflate: {
+          threshold: 1024
+        },
+        transportOptions: {},
+        closeOnBeforeunload: false
+      }, opts);
+      _this.opts.path = _this.opts.path.replace(/\/$/, "") + (_this.opts.addTrailingSlash ? "/" : "");
+      if (typeof _this.opts.query === "string") {
+        _this.opts.query = decode(_this.opts.query);
+      }
+      // set on handshake
+      _this.id = null;
+      _this.upgrades = null;
+      _this.pingInterval = null;
+      _this.pingTimeout = null;
+      // set on heartbeat
+      _this.pingTimeoutTimer = null;
+      if (typeof addEventListener === "function") {
+        if (_this.opts.closeOnBeforeunload) {
+          // Firefox closes the connection when the "beforeunload" event is emitted but not Chrome. This event listener
+          // ensures every browser behaves the same (no "disconnect" event at the Socket.IO level when the page is
+          // closed/reloaded)
+          _this.beforeunloadEventListener = function () {
+            if (_this.transport) {
+              // silently close the transport
+              _this.transport.removeAllListeners();
+              _this.transport.close();
+            }
+          };
+          addEventListener("beforeunload", _this.beforeunloadEventListener, false);
+        }
+        if (_this.hostname !== "localhost") {
+          _this.offlineEventListener = function () {
+            _this.onClose("transport close", {
+              description: "network connection lost"
+            });
+          };
+          addEventListener("offline", _this.offlineEventListener, false);
+        }
+      }
+      _this.open();
+      return _this;
+    }
+    /**
+     * Creates transport of the given type.
+     *
+     * @param {String} name - transport name
+     * @return {Transport}
+     * @private
+     */
+    _createClass(Socket, [{
+      key: "createTransport",
+      value: function createTransport(name) {
+        var query = _extends({}, this.opts.query);
+        // append engine.io protocol identifier
+        query.EIO = protocol$1;
+        // transport name
+        query.transport = name;
+        // session id if we already have one
+        if (this.id) query.sid = this.id;
+        var opts = _extends({}, this.opts, {
+          query: query,
+          socket: this,
+          hostname: this.hostname,
+          secure: this.secure,
+          port: this.port
+        }, this.opts.transportOptions[name]);
+        return new transports[name](opts);
+      }
+      /**
+       * Initializes transport to use and starts probe.
+       *
+       * @private
+       */
+    }, {
+      key: "open",
+      value: function open() {
+        var _this2 = this;
+        var transport;
+        if (this.opts.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf("websocket") !== -1) {
+          transport = "websocket";
+        } else if (0 === this.transports.length) {
+          // Emit error on next tick so it can be listened to
+          this.setTimeoutFn(function () {
+            _this2.emitReserved("error", "No transports available");
+          }, 0);
+          return;
+        } else {
+          transport = this.transports[0];
+        }
+        this.readyState = "opening";
+        // Retry with the next transport if the transport is disabled (jsonp: false)
+        try {
+          transport = this.createTransport(transport);
+        } catch (e) {
+          this.transports.shift();
+          this.open();
+          return;
+        }
+        transport.open();
+        this.setTransport(transport);
+      }
+      /**
+       * Sets the current transport. Disables the existing one (if any).
+       *
+       * @private
+       */
+    }, {
+      key: "setTransport",
+      value: function setTransport(transport) {
+        var _this3 = this;
+        if (this.transport) {
+          this.transport.removeAllListeners();
+        }
+        // set up transport
+        this.transport = transport;
+        // set up transport listeners
+        transport.on("drain", this.onDrain.bind(this)).on("packet", this.onPacket.bind(this)).on("error", this.onError.bind(this)).on("close", function (reason) {
+          return _this3.onClose("transport close", reason);
+        });
+      }
+      /**
+       * Probes a transport.
+       *
+       * @param {String} name - transport name
+       * @private
+       */
+    }, {
+      key: "probe",
+      value: function probe(name) {
+        var _this4 = this;
+        var transport = this.createTransport(name);
+        var failed = false;
+        Socket.priorWebsocketSuccess = false;
+        var onTransportOpen = function onTransportOpen() {
+          if (failed) return;
+          transport.send([{
+            type: "ping",
+            data: "probe"
+          }]);
+          transport.once("packet", function (msg) {
+            if (failed) return;
+            if ("pong" === msg.type && "probe" === msg.data) {
+              _this4.upgrading = true;
+              _this4.emitReserved("upgrading", transport);
+              if (!transport) return;
+              Socket.priorWebsocketSuccess = "websocket" === transport.name;
+              _this4.transport.pause(function () {
+                if (failed) return;
+                if ("closed" === _this4.readyState) return;
+                cleanup();
+                _this4.setTransport(transport);
+                transport.send([{
+                  type: "upgrade"
+                }]);
+                _this4.emitReserved("upgrade", transport);
+                transport = null;
+                _this4.upgrading = false;
+                _this4.flush();
+              });
+            } else {
+              var err = new Error("probe error");
+              // @ts-ignore
+              err.transport = transport.name;
+              _this4.emitReserved("upgradeError", err);
+            }
+          });
+        };
+        function freezeTransport() {
+          if (failed) return;
+          // Any callback called by transport should be ignored since now
+          failed = true;
+          cleanup();
+          transport.close();
+          transport = null;
+        }
+        // Handle any error that happens while probing
+        var onerror = function onerror(err) {
+          var error = new Error("probe error: " + err);
+          // @ts-ignore
+          error.transport = transport.name;
+          freezeTransport();
+          _this4.emitReserved("upgradeError", error);
+        };
+        function onTransportClose() {
+          onerror("transport closed");
+        }
+        // When the socket is closed while we're probing
+        function onclose() {
+          onerror("socket closed");
+        }
+        // When the socket is upgraded while we're probing
+        function onupgrade(to) {
+          if (transport && to.name !== transport.name) {
+            freezeTransport();
+          }
+        }
+        // Remove all listeners on the transport and on self
+        var cleanup = function cleanup() {
+          transport.removeListener("open", onTransportOpen);
+          transport.removeListener("error", onerror);
+          transport.removeListener("close", onTransportClose);
+          _this4.off("close", onclose);
+          _this4.off("upgrading", onupgrade);
+        };
+        transport.once("open", onTransportOpen);
+        transport.once("error", onerror);
+        transport.once("close", onTransportClose);
+        this.once("close", onclose);
+        this.once("upgrading", onupgrade);
+        if (this.upgrades.indexOf("webtransport") !== -1 && name !== "webtransport") {
+          // favor WebTransport
+          this.setTimeoutFn(function () {
+            if (!failed) {
+              transport.open();
+            }
+          }, 200);
+        } else {
+          transport.open();
+        }
+      }
+      /**
+       * Called when connection is deemed open.
+       *
+       * @private
+       */
+    }, {
+      key: "onOpen",
+      value: function onOpen() {
+        this.readyState = "open";
+        Socket.priorWebsocketSuccess = "websocket" === this.transport.name;
+        this.emitReserved("open");
+        this.flush();
+        // we check for `readyState` in case an `open`
+        // listener already closed the socket
+        if ("open" === this.readyState && this.opts.upgrade) {
+          var i = 0;
+          var l = this.upgrades.length;
+          for (; i < l; i++) {
+            this.probe(this.upgrades[i]);
+          }
+        }
+      }
+      /**
+       * Handles a packet.
+       *
+       * @private
+       */
+    }, {
+      key: "onPacket",
+      value: function onPacket(packet) {
+        if ("opening" === this.readyState || "open" === this.readyState || "closing" === this.readyState) {
+          this.emitReserved("packet", packet);
+          // Socket is live - any packet counts
+          this.emitReserved("heartbeat");
+          this.resetPingTimeout();
+          switch (packet.type) {
+            case "open":
+              this.onHandshake(JSON.parse(packet.data));
+              break;
+            case "ping":
+              this.sendPacket("pong");
+              this.emitReserved("ping");
+              this.emitReserved("pong");
+              break;
+            case "error":
+              var err = new Error("server error");
+              // @ts-ignore
+              err.code = packet.data;
+              this.onError(err);
+              break;
+            case "message":
+              this.emitReserved("data", packet.data);
+              this.emitReserved("message", packet.data);
+              break;
+          }
+        }
+      }
+      /**
+       * Called upon handshake completion.
+       *
+       * @param {Object} data - handshake obj
+       * @private
+       */
+    }, {
+      key: "onHandshake",
+      value: function onHandshake(data) {
+        this.emitReserved("handshake", data);
+        this.id = data.sid;
+        this.transport.query.sid = data.sid;
+        this.upgrades = this.filterUpgrades(data.upgrades);
+        this.pingInterval = data.pingInterval;
+        this.pingTimeout = data.pingTimeout;
+        this.maxPayload = data.maxPayload;
+        this.onOpen();
+        // In case open handler closes socket
+        if ("closed" === this.readyState) return;
+        this.resetPingTimeout();
+      }
+      /**
+       * Sets and resets ping timeout timer based on server pings.
+       *
+       * @private
+       */
+    }, {
+      key: "resetPingTimeout",
+      value: function resetPingTimeout() {
+        var _this5 = this;
+        this.clearTimeoutFn(this.pingTimeoutTimer);
+        this.pingTimeoutTimer = this.setTimeoutFn(function () {
+          _this5.onClose("ping timeout");
+        }, this.pingInterval + this.pingTimeout);
+        if (this.opts.autoUnref) {
+          this.pingTimeoutTimer.unref();
+        }
+      }
+      /**
+       * Called on `drain` event
+       *
+       * @private
+       */
+    }, {
+      key: "onDrain",
+      value: function onDrain() {
+        this.writeBuffer.splice(0, this.prevBufferLen);
+        // setting prevBufferLen = 0 is very important
+        // for example, when upgrading, upgrade packet is sent over,
+        // and a nonzero prevBufferLen could cause problems on `drain`
+        this.prevBufferLen = 0;
+        if (0 === this.writeBuffer.length) {
+          this.emitReserved("drain");
+        } else {
+          this.flush();
+        }
+      }
+      /**
+       * Flush write buffers.
+       *
+       * @private
+       */
+    }, {
+      key: "flush",
+      value: function flush() {
+        if ("closed" !== this.readyState && this.transport.writable && !this.upgrading && this.writeBuffer.length) {
+          var packets = this.getWritablePackets();
+          this.transport.send(packets);
+          // keep track of current length of writeBuffer
+          // splice writeBuffer and callbackBuffer on `drain`
+          this.prevBufferLen = packets.length;
+          this.emitReserved("flush");
+        }
+      }
+      /**
+       * Ensure the encoded size of the writeBuffer is below the maxPayload value sent by the server (only for HTTP
+       * long-polling)
+       *
+       * @private
+       */
+    }, {
+      key: "getWritablePackets",
+      value: function getWritablePackets() {
+        var shouldCheckPayloadSize = this.maxPayload && this.transport.name === "polling" && this.writeBuffer.length > 1;
+        if (!shouldCheckPayloadSize) {
+          return this.writeBuffer;
+        }
+        var payloadSize = 1; // first packet type
+        for (var i = 0; i < this.writeBuffer.length; i++) {
+          var data = this.writeBuffer[i].data;
+          if (data) {
+            payloadSize += byteLength(data);
+          }
+          if (i > 0 && payloadSize > this.maxPayload) {
+            return this.writeBuffer.slice(0, i);
+          }
+          payloadSize += 2; // separator + packet type
+        }
+
+        return this.writeBuffer;
+      }
+      /**
+       * Sends a message.
+       *
+       * @param {String} msg - message.
+       * @param {Object} options.
+       * @param {Function} callback function.
+       * @return {Socket} for chaining.
+       */
+    }, {
+      key: "write",
+      value: function write(msg, options, fn) {
+        this.sendPacket("message", msg, options, fn);
+        return this;
+      }
+    }, {
+      key: "send",
+      value: function send(msg, options, fn) {
+        this.sendPacket("message", msg, options, fn);
+        return this;
+      }
+      /**
+       * Sends a packet.
+       *
+       * @param {String} type: packet type.
+       * @param {String} data.
+       * @param {Object} options.
+       * @param {Function} fn - callback function.
+       * @private
+       */
+    }, {
+      key: "sendPacket",
+      value: function sendPacket(type, data, options, fn) {
+        if ("function" === typeof data) {
+          fn = data;
+          data = undefined;
+        }
+        if ("function" === typeof options) {
+          fn = options;
+          options = null;
+        }
+        if ("closing" === this.readyState || "closed" === this.readyState) {
+          return;
+        }
+        options = options || {};
+        options.compress = false !== options.compress;
+        var packet = {
+          type: type,
+          data: data,
+          options: options
+        };
+        this.emitReserved("packetCreate", packet);
+        this.writeBuffer.push(packet);
+        if (fn) this.once("flush", fn);
+        this.flush();
+      }
+      /**
+       * Closes the connection.
+       */
+    }, {
+      key: "close",
+      value: function close() {
+        var _this6 = this;
+        var close = function close() {
+          _this6.onClose("forced close");
+          _this6.transport.close();
+        };
+        var cleanupAndClose = function cleanupAndClose() {
+          _this6.off("upgrade", cleanupAndClose);
+          _this6.off("upgradeError", cleanupAndClose);
+          close();
+        };
+        var waitForUpgrade = function waitForUpgrade() {
+          // wait for upgrade to finish since we can't send packets while pausing a transport
+          _this6.once("upgrade", cleanupAndClose);
+          _this6.once("upgradeError", cleanupAndClose);
+        };
+        if ("opening" === this.readyState || "open" === this.readyState) {
+          this.readyState = "closing";
+          if (this.writeBuffer.length) {
+            this.once("drain", function () {
+              if (_this6.upgrading) {
+                waitForUpgrade();
+              } else {
+                close();
+              }
+            });
+          } else if (this.upgrading) {
+            waitForUpgrade();
+          } else {
+            close();
+          }
+        }
+        return this;
+      }
+      /**
+       * Called upon transport error
+       *
+       * @private
+       */
+    }, {
+      key: "onError",
+      value: function onError(err) {
+        Socket.priorWebsocketSuccess = false;
+        this.emitReserved("error", err);
+        this.onClose("transport error", err);
+      }
+      /**
+       * Called upon transport close.
+       *
+       * @private
+       */
+    }, {
+      key: "onClose",
+      value: function onClose(reason, description) {
+        if ("opening" === this.readyState || "open" === this.readyState || "closing" === this.readyState) {
+          // clear timers
+          this.clearTimeoutFn(this.pingTimeoutTimer);
+          // stop event from firing again for transport
+          this.transport.removeAllListeners("close");
+          // ensure transport won't stay open
+          this.transport.close();
+          // ignore further transport communication
+          this.transport.removeAllListeners();
+          if (typeof removeEventListener === "function") {
+            removeEventListener("beforeunload", this.beforeunloadEventListener, false);
+            removeEventListener("offline", this.offlineEventListener, false);
+          }
+          // set ready state
+          this.readyState = "closed";
+          // clear session id
+          this.id = null;
+          // emit close event
+          this.emitReserved("close", reason, description);
+          // clean buffers after, so users can still
+          // grab the buffers on `close` event
+          this.writeBuffer = [];
+          this.prevBufferLen = 0;
+        }
+      }
+      /**
+       * Filters upgrades, returning only those matching client transports.
+       *
+       * @param {Array} upgrades - server upgrades
+       * @private
+       */
+    }, {
+      key: "filterUpgrades",
+      value: function filterUpgrades(upgrades) {
+        var filteredUpgrades = [];
+        var i = 0;
+        var j = upgrades.length;
+        for (; i < j; i++) {
+          if (~this.transports.indexOf(upgrades[i])) filteredUpgrades.push(upgrades[i]);
+        }
+        return filteredUpgrades;
+      }
+    }]);
+    return Socket;
+  }(Emitter);
+  Socket$1.protocol = protocol$1;
+
+  Socket$1.protocol;
+
+  /**
+   * URL parser.
+   *
+   * @param uri - url
+   * @param path - the request path of the connection
+   * @param loc - An object meant to mimic window.location.
+   *        Defaults to window.location.
+   * @public
+   */
+  function url(uri) {
+    var path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "";
+    var loc = arguments.length > 2 ? arguments[2] : undefined;
+    var obj = uri;
+    // default to window.location
+    loc = loc || typeof location !== "undefined" && location;
+    if (null == uri) uri = loc.protocol + "//" + loc.host;
+    // relative path support
+    if (typeof uri === "string") {
+      if ("/" === uri.charAt(0)) {
+        if ("/" === uri.charAt(1)) {
+          uri = loc.protocol + uri;
+        } else {
+          uri = loc.host + uri;
+        }
+      }
+      if (!/^(https?|wss?):\/\//.test(uri)) {
+        if ("undefined" !== typeof loc) {
+          uri = loc.protocol + "//" + uri;
+        } else {
+          uri = "https://" + uri;
+        }
+      }
+      // parse
+      obj = parse(uri);
+    }
+    // make sure we treat `localhost:80` and `localhost` equally
+    if (!obj.port) {
+      if (/^(http|ws)$/.test(obj.protocol)) {
+        obj.port = "80";
+      } else if (/^(http|ws)s$/.test(obj.protocol)) {
+        obj.port = "443";
+      }
+    }
+    obj.path = obj.path || "/";
+    var ipv6 = obj.host.indexOf(":") !== -1;
+    var host = ipv6 ? "[" + obj.host + "]" : obj.host;
+    // define unique id
+    obj.id = obj.protocol + "://" + host + ":" + obj.port + path;
+    // define href
+    obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
+    return obj;
+  }
+
+  var withNativeArrayBuffer = typeof ArrayBuffer === "function";
+  var isView = function isView(obj) {
+    return typeof ArrayBuffer.isView === "function" ? ArrayBuffer.isView(obj) : obj.buffer instanceof ArrayBuffer;
+  };
+  var toString = Object.prototype.toString;
+  var withNativeBlob = typeof Blob === "function" || typeof Blob !== "undefined" && toString.call(Blob) === "[object BlobConstructor]";
+  var withNativeFile = typeof File === "function" || typeof File !== "undefined" && toString.call(File) === "[object FileConstructor]";
+  /**
+   * Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
+   *
+   * @private
+   */
+  function isBinary(obj) {
+    return withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj)) || withNativeBlob && obj instanceof Blob || withNativeFile && obj instanceof File;
+  }
+  function hasBinary(obj, toJSON) {
+    if (!obj || _typeof(obj) !== "object") {
+      return false;
+    }
+    if (Array.isArray(obj)) {
+      for (var i = 0, l = obj.length; i < l; i++) {
+        if (hasBinary(obj[i])) {
+          return true;
+        }
+      }
+      return false;
+    }
+    if (isBinary(obj)) {
+      return true;
+    }
+    if (obj.toJSON && typeof obj.toJSON === "function" && arguments.length === 1) {
+      return hasBinary(obj.toJSON(), true);
+    }
+    for (var key in obj) {
+      if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
+   *
+   * @param {Object} packet - socket.io event packet
+   * @return {Object} with deconstructed packet and list of buffers
+   * @public
+   */
+  function deconstructPacket(packet) {
+    var buffers = [];
+    var packetData = packet.data;
+    var pack = packet;
+    pack.data = _deconstructPacket(packetData, buffers);
+    pack.attachments = buffers.length; // number of binary 'attachments'
+    return {
+      packet: pack,
+      buffers: buffers
+    };
+  }
+  function _deconstructPacket(data, buffers) {
+    if (!data) return data;
+    if (isBinary(data)) {
+      var placeholder = {
+        _placeholder: true,
+        num: buffers.length
+      };
+      buffers.push(data);
+      return placeholder;
+    } else if (Array.isArray(data)) {
+      var newData = new Array(data.length);
+      for (var i = 0; i < data.length; i++) {
+        newData[i] = _deconstructPacket(data[i], buffers);
+      }
+      return newData;
+    } else if (_typeof(data) === "object" && !(data instanceof Date)) {
+      var _newData = {};
+      for (var key in data) {
+        if (Object.prototype.hasOwnProperty.call(data, key)) {
+          _newData[key] = _deconstructPacket(data[key], buffers);
+        }
+      }
+      return _newData;
+    }
+    return data;
+  }
+  /**
+   * Reconstructs a binary packet from its placeholder packet and buffers
+   *
+   * @param {Object} packet - event packet with placeholders
+   * @param {Array} buffers - binary buffers to put in placeholder positions
+   * @return {Object} reconstructed packet
+   * @public
+   */
+  function reconstructPacket(packet, buffers) {
+    packet.data = _reconstructPacket(packet.data, buffers);
+    delete packet.attachments; // no longer useful
+    return packet;
+  }
+  function _reconstructPacket(data, buffers) {
+    if (!data) return data;
+    if (data && data._placeholder === true) {
+      var isIndexValid = typeof data.num === "number" && data.num >= 0 && data.num < buffers.length;
+      if (isIndexValid) {
+        return buffers[data.num]; // appropriate buffer (should be natural order anyway)
+      } else {
+        throw new Error("illegal attachments");
+      }
+    } else if (Array.isArray(data)) {
+      for (var i = 0; i < data.length; i++) {
+        data[i] = _reconstructPacket(data[i], buffers);
+      }
+    } else if (_typeof(data) === "object") {
+      for (var key in data) {
+        if (Object.prototype.hasOwnProperty.call(data, key)) {
+          data[key] = _reconstructPacket(data[key], buffers);
+        }
+      }
+    }
+    return data;
+  }
+
+  /**
+   * These strings must not be used as event names, as they have a special meaning.
+   */
+  var RESERVED_EVENTS$1 = ["connect", "connect_error", "disconnect", "disconnecting", "newListener", "removeListener" // used by the Node.js EventEmitter
+  ];
+  /**
+   * Protocol version.
+   *
+   * @public
+   */
+  var protocol = 5;
+  var PacketType;
+  (function (PacketType) {
+    PacketType[PacketType["CONNECT"] = 0] = "CONNECT";
+    PacketType[PacketType["DISCONNECT"] = 1] = "DISCONNECT";
+    PacketType[PacketType["EVENT"] = 2] = "EVENT";
+    PacketType[PacketType["ACK"] = 3] = "ACK";
+    PacketType[PacketType["CONNECT_ERROR"] = 4] = "CONNECT_ERROR";
+    PacketType[PacketType["BINARY_EVENT"] = 5] = "BINARY_EVENT";
+    PacketType[PacketType["BINARY_ACK"] = 6] = "BINARY_ACK";
+  })(PacketType || (PacketType = {}));
+  /**
+   * A socket.io Encoder instance
+   */
+  var Encoder = /*#__PURE__*/function () {
+    /**
+     * Encoder constructor
+     *
+     * @param {function} replacer - custom replacer to pass down to JSON.parse
+     */
+    function Encoder(replacer) {
+      _classCallCheck(this, Encoder);
+      this.replacer = replacer;
+    }
+    /**
+     * Encode a packet as a single string if non-binary, or as a
+     * buffer sequence, depending on packet type.
+     *
+     * @param {Object} obj - packet object
+     */
+    _createClass(Encoder, [{
+      key: "encode",
+      value: function encode(obj) {
+        if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
+          if (hasBinary(obj)) {
+            return this.encodeAsBinary({
+              type: obj.type === PacketType.EVENT ? PacketType.BINARY_EVENT : PacketType.BINARY_ACK,
+              nsp: obj.nsp,
+              data: obj.data,
+              id: obj.id
+            });
+          }
+        }
+        return [this.encodeAsString(obj)];
+      }
+      /**
+       * Encode packet as string.
+       */
+    }, {
+      key: "encodeAsString",
+      value: function encodeAsString(obj) {
+        // first is type
+        var str = "" + obj.type;
+        // attachments if we have them
+        if (obj.type === PacketType.BINARY_EVENT || obj.type === PacketType.BINARY_ACK) {
+          str += obj.attachments + "-";
+        }
+        // if we have a namespace other than `/`
+        // we append it followed by a comma `,`
+        if (obj.nsp && "/" !== obj.nsp) {
+          str += obj.nsp + ",";
+        }
+        // immediately followed by the id
+        if (null != obj.id) {
+          str += obj.id;
+        }
+        // json data
+        if (null != obj.data) {
+          str += JSON.stringify(obj.data, this.replacer);
+        }
+        return str;
+      }
+      /**
+       * Encode packet as 'buffer sequence' by removing blobs, and
+       * deconstructing packet into object with placeholders and
+       * a list of buffers.
+       */
+    }, {
+      key: "encodeAsBinary",
+      value: function encodeAsBinary(obj) {
+        var deconstruction = deconstructPacket(obj);
+        var pack = this.encodeAsString(deconstruction.packet);
+        var buffers = deconstruction.buffers;
+        buffers.unshift(pack); // add packet info to beginning of data list
+        return buffers; // write all the buffers
+      }
+    }]);
+    return Encoder;
+  }();
+  // see https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript
+  function isObject(value) {
+    return Object.prototype.toString.call(value) === "[object Object]";
+  }
+  /**
+   * A socket.io Decoder instance
+   *
+   * @return {Object} decoder
+   */
+  var Decoder = /*#__PURE__*/function (_Emitter) {
+    _inherits(Decoder, _Emitter);
+    var _super = _createSuper(Decoder);
+    /**
+     * Decoder constructor
+     *
+     * @param {function} reviver - custom reviver to pass down to JSON.stringify
+     */
+    function Decoder(reviver) {
+      var _this;
+      _classCallCheck(this, Decoder);
+      _this = _super.call(this);
+      _this.reviver = reviver;
+      return _this;
+    }
+    /**
+     * Decodes an encoded packet string into packet JSON.
+     *
+     * @param {String} obj - encoded packet
+     */
+    _createClass(Decoder, [{
+      key: "add",
+      value: function add(obj) {
+        var packet;
+        if (typeof obj === "string") {
+          if (this.reconstructor) {
+            throw new Error("got plaintext data when reconstructing a packet");
+          }
+          packet = this.decodeString(obj);
+          var isBinaryEvent = packet.type === PacketType.BINARY_EVENT;
+          if (isBinaryEvent || packet.type === PacketType.BINARY_ACK) {
+            packet.type = isBinaryEvent ? PacketType.EVENT : PacketType.ACK;
+            // binary packet's json
+            this.reconstructor = new BinaryReconstructor(packet);
+            // no attachments, labeled binary but no binary data to follow
+            if (packet.attachments === 0) {
+              _get(_getPrototypeOf(Decoder.prototype), "emitReserved", this).call(this, "decoded", packet);
+            }
+          } else {
+            // non-binary full packet
+            _get(_getPrototypeOf(Decoder.prototype), "emitReserved", this).call(this, "decoded", packet);
+          }
+        } else if (isBinary(obj) || obj.base64) {
+          // raw binary data
+          if (!this.reconstructor) {
+            throw new Error("got binary data when not reconstructing a packet");
+          } else {
+            packet = this.reconstructor.takeBinaryData(obj);
+            if (packet) {
+              // received final buffer
+              this.reconstructor = null;
+              _get(_getPrototypeOf(Decoder.prototype), "emitReserved", this).call(this, "decoded", packet);
+            }
+          }
+        } else {
+          throw new Error("Unknown type: " + obj);
+        }
+      }
+      /**
+       * Decode a packet String (JSON data)
+       *
+       * @param {String} str
+       * @return {Object} packet
+       */
+    }, {
+      key: "decodeString",
+      value: function decodeString(str) {
+        var i = 0;
+        // look up type
+        var p = {
+          type: Number(str.charAt(0))
+        };
+        if (PacketType[p.type] === undefined) {
+          throw new Error("unknown packet type " + p.type);
+        }
+        // look up attachments if type binary
+        if (p.type === PacketType.BINARY_EVENT || p.type === PacketType.BINARY_ACK) {
+          var start = i + 1;
+          while (str.charAt(++i) !== "-" && i != str.length) {}
+          var buf = str.substring(start, i);
+          if (buf != Number(buf) || str.charAt(i) !== "-") {
+            throw new Error("Illegal attachments");
+          }
+          p.attachments = Number(buf);
+        }
+        // look up namespace (if any)
+        if ("/" === str.charAt(i + 1)) {
+          var _start = i + 1;
+          while (++i) {
+            var c = str.charAt(i);
+            if ("," === c) break;
+            if (i === str.length) break;
+          }
+          p.nsp = str.substring(_start, i);
+        } else {
+          p.nsp = "/";
+        }
+        // look up id
+        var next = str.charAt(i + 1);
+        if ("" !== next && Number(next) == next) {
+          var _start2 = i + 1;
+          while (++i) {
+            var _c = str.charAt(i);
+            if (null == _c || Number(_c) != _c) {
+              --i;
+              break;
+            }
+            if (i === str.length) break;
+          }
+          p.id = Number(str.substring(_start2, i + 1));
+        }
+        // look up json data
+        if (str.charAt(++i)) {
+          var payload = this.tryParse(str.substr(i));
+          if (Decoder.isPayloadValid(p.type, payload)) {
+            p.data = payload;
+          } else {
+            throw new Error("invalid payload");
+          }
+        }
+        return p;
+      }
+    }, {
+      key: "tryParse",
+      value: function tryParse(str) {
+        try {
+          return JSON.parse(str, this.reviver);
+        } catch (e) {
+          return false;
+        }
+      }
+    }, {
+      key: "destroy",
+      value:
+      /**
+       * Deallocates a parser's resources
+       */
+      function destroy() {
+        if (this.reconstructor) {
+          this.reconstructor.finishedReconstruction();
+          this.reconstructor = null;
+        }
+      }
+    }], [{
+      key: "isPayloadValid",
+      value: function isPayloadValid(type, payload) {
+        switch (type) {
+          case PacketType.CONNECT:
+            return isObject(payload);
+          case PacketType.DISCONNECT:
+            return payload === undefined;
+          case PacketType.CONNECT_ERROR:
+            return typeof payload === "string" || isObject(payload);
+          case PacketType.EVENT:
+          case PacketType.BINARY_EVENT:
+            return Array.isArray(payload) && (typeof payload[0] === "number" || typeof payload[0] === "string" && RESERVED_EVENTS$1.indexOf(payload[0]) === -1);
+          case PacketType.ACK:
+          case PacketType.BINARY_ACK:
+            return Array.isArray(payload);
+        }
+      }
+    }]);
+    return Decoder;
+  }(Emitter);
+  /**
+   * A manager of a binary event's 'buffer sequence'. Should
+   * be constructed whenever a packet of type BINARY_EVENT is
+   * decoded.
+   *
+   * @param {Object} packet
+   * @return {BinaryReconstructor} initialized reconstructor
+   */
+  var BinaryReconstructor = /*#__PURE__*/function () {
+    function BinaryReconstructor(packet) {
+      _classCallCheck(this, BinaryReconstructor);
+      this.packet = packet;
+      this.buffers = [];
+      this.reconPack = packet;
+    }
+    /**
+     * Method to be called when binary data received from connection
+     * after a BINARY_EVENT packet.
+     *
+     * @param {Buffer | ArrayBuffer} binData - the raw binary data received
+     * @return {null | Object} returns null if more binary data is expected or
+     *   a reconstructed packet object if all buffers have been received.
+     */
+    _createClass(BinaryReconstructor, [{
+      key: "takeBinaryData",
+      value: function takeBinaryData(binData) {
+        this.buffers.push(binData);
+        if (this.buffers.length === this.reconPack.attachments) {
+          // done with buffer list
+          var packet = reconstructPacket(this.reconPack, this.buffers);
+          this.finishedReconstruction();
+          return packet;
+        }
+        return null;
+      }
+      /**
+       * Cleans up binary packet reconstruction variables.
+       */
+    }, {
+      key: "finishedReconstruction",
+      value: function finishedReconstruction() {
+        this.reconPack = null;
+        this.buffers = [];
+      }
+    }]);
+    return BinaryReconstructor;
+  }();
+
+  var parser = /*#__PURE__*/Object.freeze({
+    __proto__: null,
+    protocol: protocol,
+    get PacketType () { return PacketType; },
+    Encoder: Encoder,
+    Decoder: Decoder
+  });
+
+  function on(obj, ev, fn) {
+    obj.on(ev, fn);
+    return function subDestroy() {
+      obj.off(ev, fn);
+    };
+  }
+
+  /**
+   * Internal events.
+   * These events can't be emitted by the user.
+   */
+  var RESERVED_EVENTS = Object.freeze({
+    connect: 1,
+    connect_error: 1,
+    disconnect: 1,
+    disconnecting: 1,
+    // EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
+    newListener: 1,
+    removeListener: 1
+  });
+  /**
+   * A Socket is the fundamental class for interacting with the server.
+   *
+   * A Socket belongs to a certain Namespace (by default /) and uses an underlying {@link Manager} to communicate.
+   *
+   * @example
+   * const socket = io();
+   *
+   * socket.on("connect", () => {
+   *   console.log("connected");
+   * });
+   *
+   * // send an event to the server
+   * socket.emit("foo", "bar");
+   *
+   * socket.on("foobar", () => {
+   *   // an event was received from the server
+   * });
+   *
+   * // upon disconnection
+   * socket.on("disconnect", (reason) => {
+   *   console.log(`disconnected due to ${reason}`);
+   * });
+   */
+  var Socket = /*#__PURE__*/function (_Emitter) {
+    _inherits(Socket, _Emitter);
+    var _super = _createSuper(Socket);
+    /**
+     * `Socket` constructor.
+     */
+    function Socket(io, nsp, opts) {
+      var _this;
+      _classCallCheck(this, Socket);
+      _this = _super.call(this);
+      /**
+       * Whether the socket is currently connected to the server.
+       *
+       * @example
+       * const socket = io();
+       *
+       * socket.on("connect", () => {
+       *   console.log(socket.connected); // true
+       * });
+       *
+       * socket.on("disconnect", () => {
+       *   console.log(socket.connected); // false
+       * });
+       */
+      _this.connected = false;
+      /**
+       * Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will
+       * be transmitted by the server.
+       */
+      _this.recovered = false;
+      /**
+       * Buffer for packets received before the CONNECT packet
+       */
+      _this.receiveBuffer = [];
+      /**
+       * Buffer for packets that will be sent once the socket is connected
+       */
+      _this.sendBuffer = [];
+      /**
+       * The queue of packets to be sent with retry in case of failure.
+       *
+       * Packets are sent one by one, each waiting for the server acknowledgement, in order to guarantee the delivery order.
+       * @private
+       */
+      _this._queue = [];
+      /**
+       * A sequence to generate the ID of the {@link QueuedPacket}.
+       * @private
+       */
+      _this._queueSeq = 0;
+      _this.ids = 0;
+      _this.acks = {};
+      _this.flags = {};
+      _this.io = io;
+      _this.nsp = nsp;
+      if (opts && opts.auth) {
+        _this.auth = opts.auth;
+      }
+      _this._opts = _extends({}, opts);
+      if (_this.io._autoConnect) _this.open();
+      return _this;
+    }
+    /**
+     * Whether the socket is currently disconnected
+     *
+     * @example
+     * const socket = io();
+     *
+     * socket.on("connect", () => {
+     *   console.log(socket.disconnected); // false
+     * });
+     *
+     * socket.on("disconnect", () => {
+     *   console.log(socket.disconnected); // true
+     * });
+     */
+    _createClass(Socket, [{
+      key: "disconnected",
+      get: function get() {
+        return !this.connected;
+      }
+      /**
+       * Subscribe to open, close and packet events
+       *
+       * @private
+       */
+    }, {
+      key: "subEvents",
+      value: function subEvents() {
+        if (this.subs) return;
+        var io = this.io;
+        this.subs = [on(io, "open", this.onopen.bind(this)), on(io, "packet", this.onpacket.bind(this)), on(io, "error", this.onerror.bind(this)), on(io, "close", this.onclose.bind(this))];
+      }
+      /**
+       * Whether the Socket will try to reconnect when its Manager connects or reconnects.
+       *
+       * @example
+       * const socket = io();
+       *
+       * console.log(socket.active); // true
+       *
+       * socket.on("disconnect", (reason) => {
+       *   if (reason === "io server disconnect") {
+       *     // the disconnection was initiated by the server, you need to manually reconnect
+       *     console.log(socket.active); // false
+       *   }
+       *   // else the socket will automatically try to reconnect
+       *   console.log(socket.active); // true
+       * });
+       */
+    }, {
+      key: "active",
+      get: function get() {
+        return !!this.subs;
+      }
+      /**
+       * "Opens" the socket.
+       *
+       * @example
+       * const socket = io({
+       *   autoConnect: false
+       * });
+       *
+       * socket.connect();
+       */
+    }, {
+      key: "connect",
+      value: function connect() {
+        if (this.connected) return this;
+        this.subEvents();
+        if (!this.io["_reconnecting"]) this.io.open(); // ensure open
+        if ("open" === this.io._readyState) this.onopen();
+        return this;
+      }
+      /**
+       * Alias for {@link connect()}.
+       */
+    }, {
+      key: "open",
+      value: function open() {
+        return this.connect();
+      }
+      /**
+       * Sends a `message` event.
+       *
+       * This method mimics the WebSocket.send() method.
+       *
+       * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
+       *
+       * @example
+       * socket.send("hello");
+       *
+       * // this is equivalent to
+       * socket.emit("message", "hello");
+       *
+       * @return self
+       */
+    }, {
+      key: "send",
+      value: function send() {
+        for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
+          args[_key] = arguments[_key];
+        }
+        args.unshift("message");
+        this.emit.apply(this, args);
+        return this;
+      }
+      /**
+       * Override `emit`.
+       * If the event is in `events`, it's emitted normally.
+       *
+       * @example
+       * socket.emit("hello", "world");
+       *
+       * // all serializable datastructures are supported (no need to call JSON.stringify)
+       * socket.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
+       *
+       * // with an acknowledgement from the server
+       * socket.emit("hello", "world", (val) => {
+       *   // ...
+       * });
+       *
+       * @return self
+       */
+    }, {
+      key: "emit",
+      value: function emit(ev) {
+        if (RESERVED_EVENTS.hasOwnProperty(ev)) {
+          throw new Error('"' + ev.toString() + '" is a reserved event name');
+        }
+        for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
+          args[_key2 - 1] = arguments[_key2];
+        }
+        args.unshift(ev);
+        if (this._opts.retries && !this.flags.fromQueue && !this.flags["volatile"]) {
+          this._addToQueue(args);
+          return this;
+        }
+        var packet = {
+          type: PacketType.EVENT,
+          data: args
+        };
+        packet.options = {};
+        packet.options.compress = this.flags.compress !== false;
+        // event ack callback
+        if ("function" === typeof args[args.length - 1]) {
+          var id = this.ids++;
+          var ack = args.pop();
+          this._registerAckCallback(id, ack);
+          packet.id = id;
+        }
+        var isTransportWritable = this.io.engine && this.io.engine.transport && this.io.engine.transport.writable;
+        var discardPacket = this.flags["volatile"] && (!isTransportWritable || !this.connected);
+        if (discardPacket) ; else if (this.connected) {
+          this.notifyOutgoingListeners(packet);
+          this.packet(packet);
+        } else {
+          this.sendBuffer.push(packet);
+        }
+        this.flags = {};
+        return this;
+      }
+      /**
+       * @private
+       */
+    }, {
+      key: "_registerAckCallback",
+      value: function _registerAckCallback(id, ack) {
+        var _this2 = this;
+        var _a;
+        var timeout = (_a = this.flags.timeout) !== null && _a !== void 0 ? _a : this._opts.ackTimeout;
+        if (timeout === undefined) {
+          this.acks[id] = ack;
+          return;
+        }
+        // @ts-ignore
+        var timer = this.io.setTimeoutFn(function () {
+          delete _this2.acks[id];
+          for (var i = 0; i < _this2.sendBuffer.length; i++) {
+            if (_this2.sendBuffer[i].id === id) {
+              _this2.sendBuffer.splice(i, 1);
+            }
+          }
+          ack.call(_this2, new Error("operation has timed out"));
+        }, timeout);
+        this.acks[id] = function () {
+          // @ts-ignore
+          _this2.io.clearTimeoutFn(timer);
+          for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
+            args[_key3] = arguments[_key3];
+          }
+          ack.apply(_this2, [null].concat(args));
+        };
+      }
+      /**
+       * Emits an event and waits for an acknowledgement
+       *
+       * @example
+       * // without timeout
+       * const response = await socket.emitWithAck("hello", "world");
+       *
+       * // with a specific timeout
+       * try {
+       *   const response = await socket.timeout(1000).emitWithAck("hello", "world");
+       * } catch (err) {
+       *   // the server did not acknowledge the event in the given delay
+       * }
+       *
+       * @return a Promise that will be fulfilled when the server acknowledges the event
+       */
+    }, {
+      key: "emitWithAck",
+      value: function emitWithAck(ev) {
+        var _this3 = this;
+        for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
+          args[_key4 - 1] = arguments[_key4];
+        }
+        // the timeout flag is optional
+        var withErr = this.flags.timeout !== undefined || this._opts.ackTimeout !== undefined;
+        return new Promise(function (resolve, reject) {
+          args.push(function (arg1, arg2) {
+            if (withErr) {
+              return arg1 ? reject(arg1) : resolve(arg2);
+            } else {
+              return resolve(arg1);
+            }
+          });
+          _this3.emit.apply(_this3, [ev].concat(args));
+        });
+      }
+      /**
+       * Add the packet to the queue.
+       * @param args
+       * @private
+       */
+    }, {
+      key: "_addToQueue",
+      value: function _addToQueue(args) {
+        var _this4 = this;
+        var ack;
+        if (typeof args[args.length - 1] === "function") {
+          ack = args.pop();
+        }
+        var packet = {
+          id: this._queueSeq++,
+          tryCount: 0,
+          pending: false,
+          args: args,
+          flags: _extends({
+            fromQueue: true
+          }, this.flags)
+        };
+        args.push(function (err) {
+          if (packet !== _this4._queue[0]) {
+            // the packet has already been acknowledged
+            return;
+          }
+          var hasError = err !== null;
+          if (hasError) {
+            if (packet.tryCount > _this4._opts.retries) {
+              _this4._queue.shift();
+              if (ack) {
+                ack(err);
+              }
+            }
+          } else {
+            _this4._queue.shift();
+            if (ack) {
+              for (var _len5 = arguments.length, responseArgs = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
+                responseArgs[_key5 - 1] = arguments[_key5];
+              }
+              ack.apply(void 0, [null].concat(responseArgs));
+            }
+          }
+          packet.pending = false;
+          return _this4._drainQueue();
+        });
+        this._queue.push(packet);
+        this._drainQueue();
+      }
+      /**
+       * Send the first packet of the queue, and wait for an acknowledgement from the server.
+       * @param force - whether to resend a packet that has not been acknowledged yet
+       *
+       * @private
+       */
+    }, {
+      key: "_drainQueue",
+      value: function _drainQueue() {
+        var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
+        if (!this.connected || this._queue.length === 0) {
+          return;
+        }
+        var packet = this._queue[0];
+        if (packet.pending && !force) {
+          return;
+        }
+        packet.pending = true;
+        packet.tryCount++;
+        this.flags = packet.flags;
+        this.emit.apply(this, packet.args);
+      }
+      /**
+       * Sends a packet.
+       *
+       * @param packet
+       * @private
+       */
+    }, {
+      key: "packet",
+      value: function packet(_packet) {
+        _packet.nsp = this.nsp;
+        this.io._packet(_packet);
+      }
+      /**
+       * Called upon engine `open`.
+       *
+       * @private
+       */
+    }, {
+      key: "onopen",
+      value: function onopen() {
+        var _this5 = this;
+        if (typeof this.auth == "function") {
+          this.auth(function (data) {
+            _this5._sendConnectPacket(data);
+          });
+        } else {
+          this._sendConnectPacket(this.auth);
+        }
+      }
+      /**
+       * Sends a CONNECT packet to initiate the Socket.IO session.
+       *
+       * @param data
+       * @private
+       */
+    }, {
+      key: "_sendConnectPacket",
+      value: function _sendConnectPacket(data) {
+        this.packet({
+          type: PacketType.CONNECT,
+          data: this._pid ? _extends({
+            pid: this._pid,
+            offset: this._lastOffset
+          }, data) : data
+        });
+      }
+      /**
+       * Called upon engine or manager `error`.
+       *
+       * @param err
+       * @private
+       */
+    }, {
+      key: "onerror",
+      value: function onerror(err) {
+        if (!this.connected) {
+          this.emitReserved("connect_error", err);
+        }
+      }
+      /**
+       * Called upon engine `close`.
+       *
+       * @param reason
+       * @param description
+       * @private
+       */
+    }, {
+      key: "onclose",
+      value: function onclose(reason, description) {
+        this.connected = false;
+        delete this.id;
+        this.emitReserved("disconnect", reason, description);
+      }
+      /**
+       * Called with socket packet.
+       *
+       * @param packet
+       * @private
+       */
+    }, {
+      key: "onpacket",
+      value: function onpacket(packet) {
+        var sameNamespace = packet.nsp === this.nsp;
+        if (!sameNamespace) return;
+        switch (packet.type) {
+          case PacketType.CONNECT:
+            if (packet.data && packet.data.sid) {
+              this.onconnect(packet.data.sid, packet.data.pid);
+            } else {
+              this.emitReserved("connect_error", new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));
+            }
+            break;
+          case PacketType.EVENT:
+          case PacketType.BINARY_EVENT:
+            this.onevent(packet);
+            break;
+          case PacketType.ACK:
+          case PacketType.BINARY_ACK:
+            this.onack(packet);
+            break;
+          case PacketType.DISCONNECT:
+            this.ondisconnect();
+            break;
+          case PacketType.CONNECT_ERROR:
+            this.destroy();
+            var err = new Error(packet.data.message);
+            // @ts-ignore
+            err.data = packet.data.data;
+            this.emitReserved("connect_error", err);
+            break;
+        }
+      }
+      /**
+       * Called upon a server event.
+       *
+       * @param packet
+       * @private
+       */
+    }, {
+      key: "onevent",
+      value: function onevent(packet) {
+        var args = packet.data || [];
+        if (null != packet.id) {
+          args.push(this.ack(packet.id));
+        }
+        if (this.connected) {
+          this.emitEvent(args);
+        } else {
+          this.receiveBuffer.push(Object.freeze(args));
+        }
+      }
+    }, {
+      key: "emitEvent",
+      value: function emitEvent(args) {
+        if (this._anyListeners && this._anyListeners.length) {
+          var listeners = this._anyListeners.slice();
+          var _iterator = _createForOfIteratorHelper(listeners),
+            _step;
+          try {
+            for (_iterator.s(); !(_step = _iterator.n()).done;) {
+              var listener = _step.value;
+              listener.apply(this, args);
+            }
+          } catch (err) {
+            _iterator.e(err);
+          } finally {
+            _iterator.f();
+          }
+        }
+        _get(_getPrototypeOf(Socket.prototype), "emit", this).apply(this, args);
+        if (this._pid && args.length && typeof args[args.length - 1] === "string") {
+          this._lastOffset = args[args.length - 1];
+        }
+      }
+      /**
+       * Produces an ack callback to emit with an event.
+       *
+       * @private
+       */
+    }, {
+      key: "ack",
+      value: function ack(id) {
+        var self = this;
+        var sent = false;
+        return function () {
+          // prevent double callbacks
+          if (sent) return;
+          sent = true;
+          for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
+            args[_key6] = arguments[_key6];
+          }
+          self.packet({
+            type: PacketType.ACK,
+            id: id,
+            data: args
+          });
+        };
+      }
+      /**
+       * Called upon a server acknowlegement.
+       *
+       * @param packet
+       * @private
+       */
+    }, {
+      key: "onack",
+      value: function onack(packet) {
+        var ack = this.acks[packet.id];
+        if ("function" === typeof ack) {
+          ack.apply(this, packet.data);
+          delete this.acks[packet.id];
+        }
+      }
+      /**
+       * Called upon server connect.
+       *
+       * @private
+       */
+    }, {
+      key: "onconnect",
+      value: function onconnect(id, pid) {
+        this.id = id;
+        this.recovered = pid && this._pid === pid;
+        this._pid = pid; // defined only if connection state recovery is enabled
+        this.connected = true;
+        this.emitBuffered();
+        this.emitReserved("connect");
+        this._drainQueue(true);
+      }
+      /**
+       * Emit buffered events (received and emitted).
+       *
+       * @private
+       */
+    }, {
+      key: "emitBuffered",
+      value: function emitBuffered() {
+        var _this6 = this;
+        this.receiveBuffer.forEach(function (args) {
+          return _this6.emitEvent(args);
+        });
+        this.receiveBuffer = [];
+        this.sendBuffer.forEach(function (packet) {
+          _this6.notifyOutgoingListeners(packet);
+          _this6.packet(packet);
+        });
+        this.sendBuffer = [];
+      }
+      /**
+       * Called upon server disconnect.
+       *
+       * @private
+       */
+    }, {
+      key: "ondisconnect",
+      value: function ondisconnect() {
+        this.destroy();
+        this.onclose("io server disconnect");
+      }
+      /**
+       * Called upon forced client/server side disconnections,
+       * this method ensures the manager stops tracking us and
+       * that reconnections don't get triggered for this.
+       *
+       * @private
+       */
+    }, {
+      key: "destroy",
+      value: function destroy() {
+        if (this.subs) {
+          // clean subscriptions to avoid reconnections
+          this.subs.forEach(function (subDestroy) {
+            return subDestroy();
+          });
+          this.subs = undefined;
+        }
+        this.io["_destroy"](this);
+      }
+      /**
+       * Disconnects the socket manually. In that case, the socket will not try to reconnect.
+       *
+       * If this is the last active Socket instance of the {@link Manager}, the low-level connection will be closed.
+       *
+       * @example
+       * const socket = io();
+       *
+       * socket.on("disconnect", (reason) => {
+       *   // console.log(reason); prints "io client disconnect"
+       * });
+       *
+       * socket.disconnect();
+       *
+       * @return self
+       */
+    }, {
+      key: "disconnect",
+      value: function disconnect() {
+        if (this.connected) {
+          this.packet({
+            type: PacketType.DISCONNECT
+          });
+        }
+        // remove socket from pool
+        this.destroy();
+        if (this.connected) {
+          // fire events
+          this.onclose("io client disconnect");
+        }
+        return this;
+      }
+      /**
+       * Alias for {@link disconnect()}.
+       *
+       * @return self
+       */
+    }, {
+      key: "close",
+      value: function close() {
+        return this.disconnect();
+      }
+      /**
+       * Sets the compress flag.
+       *
+       * @example
+       * socket.compress(false).emit("hello");
+       *
+       * @param compress - if `true`, compresses the sending data
+       * @return self
+       */
+    }, {
+      key: "compress",
+      value: function compress(_compress) {
+        this.flags.compress = _compress;
+        return this;
+      }
+      /**
+       * Sets a modifier for a subsequent event emission that the event message will be dropped when this socket is not
+       * ready to send messages.
+       *
+       * @example
+       * socket.volatile.emit("hello"); // the server may or may not receive it
+       *
+       * @returns self
+       */
+    }, {
+      key: "volatile",
+      get: function get() {
+        this.flags["volatile"] = true;
+        return this;
+      }
+      /**
+       * Sets a modifier for a subsequent event emission that the callback will be called with an error when the
+       * given number of milliseconds have elapsed without an acknowledgement from the server:
+       *
+       * @example
+       * socket.timeout(5000).emit("my-event", (err) => {
+       *   if (err) {
+       *     // the server did not acknowledge the event in the given delay
+       *   }
+       * });
+       *
+       * @returns self
+       */
+    }, {
+      key: "timeout",
+      value: function timeout(_timeout) {
+        this.flags.timeout = _timeout;
+        return this;
+      }
+      /**
+       * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
+       * callback.
+       *
+       * @example
+       * socket.onAny((event, ...args) => {
+       *   console.log(`got ${event}`);
+       * });
+       *
+       * @param listener
+       */
+    }, {
+      key: "onAny",
+      value: function onAny(listener) {
+        this._anyListeners = this._anyListeners || [];
+        this._anyListeners.push(listener);
+        return this;
+      }
+      /**
+       * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
+       * callback. The listener is added to the beginning of the listeners array.
+       *
+       * @example
+       * socket.prependAny((event, ...args) => {
+       *   console.log(`got event ${event}`);
+       * });
+       *
+       * @param listener
+       */
+    }, {
+      key: "prependAny",
+      value: function prependAny(listener) {
+        this._anyListeners = this._anyListeners || [];
+        this._anyListeners.unshift(listener);
+        return this;
+      }
+      /**
+       * Removes the listener that will be fired when any event is emitted.
+       *
+       * @example
+       * const catchAllListener = (event, ...args) => {
+       *   console.log(`got event ${event}`);
+       * }
+       *
+       * socket.onAny(catchAllListener);
+       *
+       * // remove a specific listener
+       * socket.offAny(catchAllListener);
+       *
+       * // or remove all listeners
+       * socket.offAny();
+       *
+       * @param listener
+       */
+    }, {
+      key: "offAny",
+      value: function offAny(listener) {
+        if (!this._anyListeners) {
+          return this;
+        }
+        if (listener) {
+          var listeners = this._anyListeners;
+          for (var i = 0; i < listeners.length; i++) {
+            if (listener === listeners[i]) {
+              listeners.splice(i, 1);
+              return this;
+            }
+          }
+        } else {
+          this._anyListeners = [];
+        }
+        return this;
+      }
+      /**
+       * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
+       * e.g. to remove listeners.
+       */
+    }, {
+      key: "listenersAny",
+      value: function listenersAny() {
+        return this._anyListeners || [];
+      }
+      /**
+       * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
+       * callback.
+       *
+       * Note: acknowledgements sent to the server are not included.
+       *
+       * @example
+       * socket.onAnyOutgoing((event, ...args) => {
+       *   console.log(`sent event ${event}`);
+       * });
+       *
+       * @param listener
+       */
+    }, {
+      key: "onAnyOutgoing",
+      value: function onAnyOutgoing(listener) {
+        this._anyOutgoingListeners = this._anyOutgoingListeners || [];
+        this._anyOutgoingListeners.push(listener);
+        return this;
+      }
+      /**
+       * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
+       * callback. The listener is added to the beginning of the listeners array.
+       *
+       * Note: acknowledgements sent to the server are not included.
+       *
+       * @example
+       * socket.prependAnyOutgoing((event, ...args) => {
+       *   console.log(`sent event ${event}`);
+       * });
+       *
+       * @param listener
+       */
+    }, {
+      key: "prependAnyOutgoing",
+      value: function prependAnyOutgoing(listener) {
+        this._anyOutgoingListeners = this._anyOutgoingListeners || [];
+        this._anyOutgoingListeners.unshift(listener);
+        return this;
+      }
+      /**
+       * Removes the listener that will be fired when any event is emitted.
+       *
+       * @example
+       * const catchAllListener = (event, ...args) => {
+       *   console.log(`sent event ${event}`);
+       * }
+       *
+       * socket.onAnyOutgoing(catchAllListener);
+       *
+       * // remove a specific listener
+       * socket.offAnyOutgoing(catchAllListener);
+       *
+       * // or remove all listeners
+       * socket.offAnyOutgoing();
+       *
+       * @param [listener] - the catch-all listener (optional)
+       */
+    }, {
+      key: "offAnyOutgoing",
+      value: function offAnyOutgoing(listener) {
+        if (!this._anyOutgoingListeners) {
+          return this;
+        }
+        if (listener) {
+          var listeners = this._anyOutgoingListeners;
+          for (var i = 0; i < listeners.length; i++) {
+            if (listener === listeners[i]) {
+              listeners.splice(i, 1);
+              return this;
+            }
+          }
+        } else {
+          this._anyOutgoingListeners = [];
+        }
+        return this;
+      }
+      /**
+       * Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
+       * e.g. to remove listeners.
+       */
+    }, {
+      key: "listenersAnyOutgoing",
+      value: function listenersAnyOutgoing() {
+        return this._anyOutgoingListeners || [];
+      }
+      /**
+       * Notify the listeners for each packet sent
+       *
+       * @param packet
+       *
+       * @private
+       */
+    }, {
+      key: "notifyOutgoingListeners",
+      value: function notifyOutgoingListeners(packet) {
+        if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
+          var listeners = this._anyOutgoingListeners.slice();
+          var _iterator2 = _createForOfIteratorHelper(listeners),
+            _step2;
+          try {
+            for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
+              var listener = _step2.value;
+              listener.apply(this, packet.data);
+            }
+          } catch (err) {
+            _iterator2.e(err);
+          } finally {
+            _iterator2.f();
+          }
+        }
+      }
+    }]);
+    return Socket;
+  }(Emitter);
+
+  /**
+   * Initialize backoff timer with `opts`.
+   *
+   * - `min` initial timeout in milliseconds [100]
+   * - `max` max timeout [10000]
+   * - `jitter` [0]
+   * - `factor` [2]
+   *
+   * @param {Object} opts
+   * @api public
+   */
+  function Backoff(opts) {
+    opts = opts || {};
+    this.ms = opts.min || 100;
+    this.max = opts.max || 10000;
+    this.factor = opts.factor || 2;
+    this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0;
+    this.attempts = 0;
+  }
+  /**
+   * Return the backoff duration.
+   *
+   * @return {Number}
+   * @api public
+   */
+  Backoff.prototype.duration = function () {
+    var ms = this.ms * Math.pow(this.factor, this.attempts++);
+    if (this.jitter) {
+      var rand = Math.random();
+      var deviation = Math.floor(rand * this.jitter * ms);
+      ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation;
+    }
+    return Math.min(ms, this.max) | 0;
+  };
+  /**
+   * Reset the number of attempts.
+   *
+   * @api public
+   */
+  Backoff.prototype.reset = function () {
+    this.attempts = 0;
+  };
+  /**
+   * Set the minimum duration
+   *
+   * @api public
+   */
+  Backoff.prototype.setMin = function (min) {
+    this.ms = min;
+  };
+  /**
+   * Set the maximum duration
+   *
+   * @api public
+   */
+  Backoff.prototype.setMax = function (max) {
+    this.max = max;
+  };
+  /**
+   * Set the jitter
+   *
+   * @api public
+   */
+  Backoff.prototype.setJitter = function (jitter) {
+    this.jitter = jitter;
+  };
+
+  var Manager = /*#__PURE__*/function (_Emitter) {
+    _inherits(Manager, _Emitter);
+    var _super = _createSuper(Manager);
+    function Manager(uri, opts) {
+      var _this;
+      _classCallCheck(this, Manager);
+      var _a;
+      _this = _super.call(this);
+      _this.nsps = {};
+      _this.subs = [];
+      if (uri && "object" === _typeof(uri)) {
+        opts = uri;
+        uri = undefined;
+      }
+      opts = opts || {};
+      opts.path = opts.path || "/socket.io";
+      _this.opts = opts;
+      installTimerFunctions(_assertThisInitialized(_this), opts);
+      _this.reconnection(opts.reconnection !== false);
+      _this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
+      _this.reconnectionDelay(opts.reconnectionDelay || 1000);
+      _this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
+      _this.randomizationFactor((_a = opts.randomizationFactor) !== null && _a !== void 0 ? _a : 0.5);
+      _this.backoff = new Backoff({
+        min: _this.reconnectionDelay(),
+        max: _this.reconnectionDelayMax(),
+        jitter: _this.randomizationFactor()
+      });
+      _this.timeout(null == opts.timeout ? 20000 : opts.timeout);
+      _this._readyState = "closed";
+      _this.uri = uri;
+      var _parser = opts.parser || parser;
+      _this.encoder = new _parser.Encoder();
+      _this.decoder = new _parser.Decoder();
+      _this._autoConnect = opts.autoConnect !== false;
+      if (_this._autoConnect) _this.open();
+      return _this;
+    }
+    _createClass(Manager, [{
+      key: "reconnection",
+      value: function reconnection(v) {
+        if (!arguments.length) return this._reconnection;
+        this._reconnection = !!v;
+        return this;
+      }
+    }, {
+      key: "reconnectionAttempts",
+      value: function reconnectionAttempts(v) {
+        if (v === undefined) return this._reconnectionAttempts;
+        this._reconnectionAttempts = v;
+        return this;
+      }
+    }, {
+      key: "reconnectionDelay",
+      value: function reconnectionDelay(v) {
+        var _a;
+        if (v === undefined) return this._reconnectionDelay;
+        this._reconnectionDelay = v;
+        (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMin(v);
+        return this;
+      }
+    }, {
+      key: "randomizationFactor",
+      value: function randomizationFactor(v) {
+        var _a;
+        if (v === undefined) return this._randomizationFactor;
+        this._randomizationFactor = v;
+        (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setJitter(v);
+        return this;
+      }
+    }, {
+      key: "reconnectionDelayMax",
+      value: function reconnectionDelayMax(v) {
+        var _a;
+        if (v === undefined) return this._reconnectionDelayMax;
+        this._reconnectionDelayMax = v;
+        (_a = this.backoff) === null || _a === void 0 ? void 0 : _a.setMax(v);
+        return this;
+      }
+    }, {
+      key: "timeout",
+      value: function timeout(v) {
+        if (!arguments.length) return this._timeout;
+        this._timeout = v;
+        return this;
+      }
+      /**
+       * Starts trying to reconnect if reconnection is enabled and we have not
+       * started reconnecting yet
+       *
+       * @private
+       */
+    }, {
+      key: "maybeReconnectOnOpen",
+      value: function maybeReconnectOnOpen() {
+        // Only try to reconnect if it's the first time we're connecting
+        if (!this._reconnecting && this._reconnection && this.backoff.attempts === 0) {
+          // keeps reconnection from firing twice for the same reconnection loop
+          this.reconnect();
+        }
+      }
+      /**
+       * Sets the current transport `socket`.
+       *
+       * @param {Function} fn - optional, callback
+       * @return self
+       * @public
+       */
+    }, {
+      key: "open",
+      value: function open(fn) {
+        var _this2 = this;
+        if (~this._readyState.indexOf("open")) return this;
+        this.engine = new Socket$1(this.uri, this.opts);
+        var socket = this.engine;
+        var self = this;
+        this._readyState = "opening";
+        this.skipReconnect = false;
+        // emit `open`
+        var openSubDestroy = on(socket, "open", function () {
+          self.onopen();
+          fn && fn();
+        });
+        var onError = function onError(err) {
+          _this2.cleanup();
+          _this2._readyState = "closed";
+          _this2.emitReserved("error", err);
+          if (fn) {
+            fn(err);
+          } else {
+            // Only do this if there is no fn to handle the error
+            _this2.maybeReconnectOnOpen();
+          }
+        };
+        // emit `error`
+        var errorSub = on(socket, "error", onError);
+        if (false !== this._timeout) {
+          var timeout = this._timeout;
+          // set timer
+          var timer = this.setTimeoutFn(function () {
+            openSubDestroy();
+            onError(new Error("timeout"));
+            socket.close();
+          }, timeout);
+          if (this.opts.autoUnref) {
+            timer.unref();
+          }
+          this.subs.push(function () {
+            _this2.clearTimeoutFn(timer);
+          });
+        }
+        this.subs.push(openSubDestroy);
+        this.subs.push(errorSub);
+        return this;
+      }
+      /**
+       * Alias for open()
+       *
+       * @return self
+       * @public
+       */
+    }, {
+      key: "connect",
+      value: function connect(fn) {
+        return this.open(fn);
+      }
+      /**
+       * Called upon transport open.
+       *
+       * @private
+       */
+    }, {
+      key: "onopen",
+      value: function onopen() {
+        // clear old subs
+        this.cleanup();
+        // mark as open
+        this._readyState = "open";
+        this.emitReserved("open");
+        // add new subs
+        var socket = this.engine;
+        this.subs.push(on(socket, "ping", this.onping.bind(this)), on(socket, "data", this.ondata.bind(this)), on(socket, "error", this.onerror.bind(this)), on(socket, "close", this.onclose.bind(this)), on(this.decoder, "decoded", this.ondecoded.bind(this)));
+      }
+      /**
+       * Called upon a ping.
+       *
+       * @private
+       */
+    }, {
+      key: "onping",
+      value: function onping() {
+        this.emitReserved("ping");
+      }
+      /**
+       * Called with data.
+       *
+       * @private
+       */
+    }, {
+      key: "ondata",
+      value: function ondata(data) {
+        try {
+          this.decoder.add(data);
+        } catch (e) {
+          this.onclose("parse error", e);
+        }
+      }
+      /**
+       * Called when parser fully decodes a packet.
+       *
+       * @private
+       */
+    }, {
+      key: "ondecoded",
+      value: function ondecoded(packet) {
+        var _this3 = this;
+        // the nextTick call prevents an exception in a user-provided event listener from triggering a disconnection due to a "parse error"
+        nextTick(function () {
+          _this3.emitReserved("packet", packet);
+        }, this.setTimeoutFn);
+      }
+      /**
+       * Called upon socket error.
+       *
+       * @private
+       */
+    }, {
+      key: "onerror",
+      value: function onerror(err) {
+        this.emitReserved("error", err);
+      }
+      /**
+       * Creates a new socket for the given `nsp`.
+       *
+       * @return {Socket}
+       * @public
+       */
+    }, {
+      key: "socket",
+      value: function socket(nsp, opts) {
+        var socket = this.nsps[nsp];
+        if (!socket) {
+          socket = new Socket(this, nsp, opts);
+          this.nsps[nsp] = socket;
+        } else if (this._autoConnect && !socket.active) {
+          socket.connect();
+        }
+        return socket;
+      }
+      /**
+       * Called upon a socket close.
+       *
+       * @param socket
+       * @private
+       */
+    }, {
+      key: "_destroy",
+      value: function _destroy(socket) {
+        var nsps = Object.keys(this.nsps);
+        for (var _i = 0, _nsps = nsps; _i < _nsps.length; _i++) {
+          var nsp = _nsps[_i];
+          var _socket = this.nsps[nsp];
+          if (_socket.active) {
+            return;
+          }
+        }
+        this._close();
+      }
+      /**
+       * Writes a packet.
+       *
+       * @param packet
+       * @private
+       */
+    }, {
+      key: "_packet",
+      value: function _packet(packet) {
+        var encodedPackets = this.encoder.encode(packet);
+        for (var i = 0; i < encodedPackets.length; i++) {
+          this.engine.write(encodedPackets[i], packet.options);
+        }
+      }
+      /**
+       * Clean up transport subscriptions and packet buffer.
+       *
+       * @private
+       */
+    }, {
+      key: "cleanup",
+      value: function cleanup() {
+        this.subs.forEach(function (subDestroy) {
+          return subDestroy();
+        });
+        this.subs.length = 0;
+        this.decoder.destroy();
+      }
+      /**
+       * Close the current socket.
+       *
+       * @private
+       */
+    }, {
+      key: "_close",
+      value: function _close() {
+        this.skipReconnect = true;
+        this._reconnecting = false;
+        this.onclose("forced close");
+        if (this.engine) this.engine.close();
+      }
+      /**
+       * Alias for close()
+       *
+       * @private
+       */
+    }, {
+      key: "disconnect",
+      value: function disconnect() {
+        return this._close();
+      }
+      /**
+       * Called upon engine close.
+       *
+       * @private
+       */
+    }, {
+      key: "onclose",
+      value: function onclose(reason, description) {
+        this.cleanup();
+        this.backoff.reset();
+        this._readyState = "closed";
+        this.emitReserved("close", reason, description);
+        if (this._reconnection && !this.skipReconnect) {
+          this.reconnect();
+        }
+      }
+      /**
+       * Attempt a reconnection.
+       *
+       * @private
+       */
+    }, {
+      key: "reconnect",
+      value: function reconnect() {
+        var _this4 = this;
+        if (this._reconnecting || this.skipReconnect) return this;
+        var self = this;
+        if (this.backoff.attempts >= this._reconnectionAttempts) {
+          this.backoff.reset();
+          this.emitReserved("reconnect_failed");
+          this._reconnecting = false;
+        } else {
+          var delay = this.backoff.duration();
+          this._reconnecting = true;
+          var timer = this.setTimeoutFn(function () {
+            if (self.skipReconnect) return;
+            _this4.emitReserved("reconnect_attempt", self.backoff.attempts);
+            // check again for the case socket closed in above events
+            if (self.skipReconnect) return;
+            self.open(function (err) {
+              if (err) {
+                self._reconnecting = false;
+                self.reconnect();
+                _this4.emitReserved("reconnect_error", err);
+              } else {
+                self.onreconnect();
+              }
+            });
+          }, delay);
+          if (this.opts.autoUnref) {
+            timer.unref();
+          }
+          this.subs.push(function () {
+            _this4.clearTimeoutFn(timer);
+          });
+        }
+      }
+      /**
+       * Called upon successful reconnect.
+       *
+       * @private
+       */
+    }, {
+      key: "onreconnect",
+      value: function onreconnect() {
+        var attempt = this.backoff.attempts;
+        this._reconnecting = false;
+        this.backoff.reset();
+        this.emitReserved("reconnect", attempt);
+      }
+    }]);
+    return Manager;
+  }(Emitter);
+
+  /**
+   * Managers cache.
+   */
+  var cache = {};
+  function lookup(uri, opts) {
+    if (_typeof(uri) === "object") {
+      opts = uri;
+      uri = undefined;
+    }
+    opts = opts || {};
+    var parsed = url(uri, opts.path || "/socket.io");
+    var source = parsed.source;
+    var id = parsed.id;
+    var path = parsed.path;
+    var sameNamespace = cache[id] && path in cache[id]["nsps"];
+    var newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
+    var io;
+    if (newConnection) {
+      io = new Manager(source, opts);
+    } else {
+      if (!cache[id]) {
+        cache[id] = new Manager(source, opts);
+      }
+      io = cache[id];
+    }
+    if (parsed.query && !opts.query) {
+      opts.query = parsed.queryKey;
+    }
+    return io.socket(parsed.path, opts);
+  }
+  // so that "lookup" can be used both as a function (e.g. `io(...)`) and as a
+  // namespace (e.g. `io.connect(...)`), for backward compatibility
+  _extends(lookup, {
+    Manager: Manager,
+    Socket: Socket,
+    io: lookup,
+    connect: lookup
+  });
+
+  return lookup;
+
+}));
+//# sourceMappingURL=socket.io.js.map

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.js.map


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.min.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.min.js.map


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.msgpack.min.js


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
packages/puter-dot-js/src/lib/socket.io/socket.io.msgpack.min.js.map


+ 436 - 0
packages/puter-dot-js/src/lib/utils.js

@@ -0,0 +1,436 @@
+/**
+ * Parses a given response text into a JSON object. If the parsing fails due to invalid JSON format,
+ * the original response text is returned.
+ *
+ * @param {string} responseText - The response text to be parsed into JSON. It is expected to be a valid JSON string.
+ * @returns {Object|string} The parsed JSON object if the responseText is valid JSON, otherwise returns the original responseText.
+ * @example
+ * // returns { key: "value" }
+ * parseResponse('{"key": "value"}');
+ *
+ * @example
+ * // returns "Invalid JSON"
+ * parseResponse('Invalid JSON');
+ */
+async function parseResponse(target) {
+    if ( target.responseType === 'blob' ) {
+        // Get content type of the blob
+        const contentType = target.getResponseHeader('content-type');
+        if ( contentType.startsWith('application/json') ) {
+            // If the blob is JSON, parse it
+            const text = await target.response.text();
+            try {
+                return JSON.parse(text);
+            } catch (error) {
+                return text;
+            }
+        }else if ( contentType.startsWith('application/octet-stream') ) {
+            // If the blob is an octet stream, return the blob
+            return target.response;
+        }
+
+        // Otherwise return an ojbect
+        return {
+            success: true,
+            result: target.response,
+        };
+    }
+    const responseText = target.responseText;
+    try {
+        return JSON.parse(responseText);
+    } catch (error) {
+        return responseText;
+    }
+}
+
+/**
+ * A function that generates a UUID (Universally Unique Identifier) using the version 4 format, 
+ * which are random UUIDs. It uses the cryptographic number generator available in modern browsers.
+ *
+ * The generated UUID is a 36 character string (32 alphanumeric characters separated by 4 hyphens). 
+ * It follows the pattern: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where x is any hexadecimal digit 
+ * and y is one of 8, 9, A, or B.
+ *
+ * @returns {string} Returns a new UUID v4 string.
+ *
+ * @example
+ * 
+ * let id = this.#uuidv4(); // Generate a new UUID
+ * 
+ */
+function uuidv4(){
+    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
+        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
+    );
+}
+
+
+/**
+ * Initializes and returns an XMLHttpRequest object configured for a specific API endpoint, method, and headers.
+ *
+ * @param {string} endpoint - The API endpoint to which the request will be sent. This is appended to the API origin URL.
+ * @param {string} APIOrigin - The origin URL of the API. This is prepended to the endpoint.
+ * @param {string} authToken - The authorization token used for accessing the API. This is included in the request headers.
+ * @param {string} [method='post'] - The HTTP method to be used for the request. Defaults to 'post' if not specified.
+ * @param {string} [contentType='application/json;charset=UTF-8'] - The content type of the request. Defaults to
+ *                                                                  'application/json;charset=UTF-8' if not specified.
+ *
+ * @returns {XMLHttpRequest} The initialized XMLHttpRequest object.
+ */
+function initXhr(endpoint, APIOrigin, authToken, method= "post", contentType = "application/json;charset=UTF-8", responseType = undefined) {
+    const xhr = new XMLHttpRequest();
+    xhr.open(method, APIOrigin + endpoint, true);
+    xhr.setRequestHeader("Authorization", "Bearer " + authToken);
+    xhr.setRequestHeader("Content-Type", contentType);
+    xhr.responseType = responseType;
+    return xhr;
+}
+
+/**
+ * Handles an HTTP response by invoking appropriate callback functions and resolving or rejecting a promise.
+ * 
+ * @param {Function} success_cb - An optional callback function for successful responses. It should take a response object
+ *                                as its only argument.
+ * @param {Function} error_cb - An optional callback function for error handling. It should take an error object
+ *                              as its only argument.
+ * @param {Function} resolve_func - A function used to resolve a promise. It should take a response object
+ *                                  as its only argument.
+ * @param {Function} reject_func - A function used to reject a promise. It should take an error object
+ *                                 as its only argument.
+ * @param {Object} response - The HTTP response object from the request. Expected to have 'status' and 'responseText'
+ *                            properties.
+ * 
+ * @returns {void} The function does not return a value but will either resolve or reject a promise based on the
+ *                 response status.
+ */
+async function handle_resp(success_cb, error_cb, resolve_func, reject_func, response){
+    const resp = await parseResponse(response);
+    // error - unauthorized
+    if(response.status === 401){
+        // if error callback is provided, call it
+        if(error_cb && typeof error_cb === 'function')
+            error_cb({status: 401, message: 'Unauthorized'})
+        // reject promise
+        return reject_func({status: 401, message: 'Unauthorized'})
+    }
+    // error - other
+    else if(response.status !== 200){
+        // if error callback is provided, call it
+        if(error_cb && typeof error_cb === 'function')
+            error_cb(resp)
+        // reject promise
+        return reject_func(resp)
+    }
+    // success
+    else{
+        // This is a driver error
+        if(resp.success === false && resp.error?.code === 'permission_denied'){
+            let perm = await puter.ui.requestPermission({permission: 'driver:puter-image-generation:generate'});
+            // try sending again if permission was granted
+            if(perm.granted){
+                // todo repeat request
+            }
+        }
+        // if success callback is provided, call it
+        if(success_cb && typeof success_cb === 'function')
+            success_cb(resp);
+        // resolve with success
+        return resolve_func(resp);
+    }
+}
+
+/**
+ * Handles an error by invoking a specified error callback and then rejecting a promise.
+ * 
+ * @param {Function} error_cb - An optional callback function that is called if it's provided.
+ *                              This function should take an error object as its only argument.
+ * @param {Function} reject_func - A function used to reject a promise. It should take an error object
+ *                                 as its only argument.
+ * @param {Object} error - The error object that is passed to both the error callback and the reject function.
+ * 
+ * @returns {void} The function does not return a value but will call the reject function with the error.
+ */
+function handle_error(error_cb, reject_func, error){
+    // if error callback is provided, call it
+    if(error_cb && typeof error_cb === 'function')
+        error_cb(error)
+    // reject promise
+    return reject_func(error)
+}
+
+function setupXhrEventHandlers(xhr, success_cb, error_cb, resolve_func, reject_func) {
+    // load: success or error
+    xhr.addEventListener('load', function(e){
+        return handle_resp(success_cb, error_cb, resolve_func, reject_func, this, xhr);
+    });
+
+    // error
+    xhr.addEventListener('error', function(e){
+        return handle_error(error_cb, reject_func, this);
+    })
+}
+
+const NOOP = () => {};
+class Valid {
+    static callback (cb) {
+        return (cb && typeof cb === 'function') ? cb : undefined;
+    }
+}
+
+/**
+ * Makes the hybrid promise/callback function for a particular driver method
+ * @param {string[]} arg_defs - argument names (for now; definitions eventually)
+ * @param {string} driverInterface - name of the interface
+ * @param {string} driverMethod - name of the method
+ */
+function make_driver_method(arg_defs, driverInterface, driverMethod, settings = {}) {
+    return async function (...args) {
+        let driverArgs = {};
+        let options = {};
+
+        // Check if the first argument is an object and use it as named parameters
+        if (args.length === 1 && typeof args[0] === 'object' && !Array.isArray(args[0])) {
+            driverArgs = { ...args[0] };
+            options = {
+                success: driverArgs.success,
+                error: driverArgs.error,
+            };
+            // Remove callback functions from driverArgs if they exist
+            delete driverArgs.success;
+            delete driverArgs.error;
+        } else {
+            // Handle as individual arguments
+            arg_defs.forEach((argName, index) => {
+                driverArgs[argName] = args[index];
+            });
+            options = {
+                success: args[arg_defs.length],
+                error: args[arg_defs.length + 1],
+            };
+        }
+
+        // preprocess
+        if(settings.preprocess && typeof settings.preprocess === 'function'){
+            driverArgs = settings.preprocess(driverArgs);
+        }
+
+        return await driverCall(options, driverInterface, driverMethod, driverArgs, settings);
+    };
+}
+
+async function driverCall (options, driverInterface, driverMethod, driverArgs, settings) {
+    const tp = new TeePromise();
+
+    driverCall_(
+        options,
+        tp.resolve.bind(tp),
+        tp.reject.bind(tp),
+        driverInterface,
+        driverMethod,
+        driverArgs,
+        undefined,
+        undefined,
+        settings,
+    );
+
+    return await tp;
+}
+
+// This function encapsulates the logic for sending a driver call request
+async function driverCall_(
+    options = {},
+    resolve_func, reject_func,
+    driverInterface, driverMethod, driverArgs,
+    method,
+    contentType = 'application/json;charset=UTF-8',
+    settings = {},
+) {
+    // If there is no authToken and the environment is web, try authenticating with Puter
+    if(!puter.authToken && puter.env === 'web'){
+        try{
+            await puter.ui.authenticateWithPuter();
+        }catch(e){
+            return reject_func({
+                error: {
+                    code: 'auth_canceled', message: 'Authentication canceled'
+                }
+            })
+        }
+    }
+
+    const success_cb = Valid.callback(options.success) ?? NOOP;
+    const error_cb = Valid.callback(options.error) ?? NOOP;
+    // create xhr object
+    const xhr = initXhr('/drivers/call', puter.APIOrigin, puter.authToken, 'POST', contentType);
+
+    if ( settings.responseType ) {
+        xhr.responseType = settings.responseType;
+    }
+
+    // load: success or error
+    xhr.addEventListener('load', async function(response){
+        const resp = await parseResponse(response.target);
+        // HTTP Error - unauthorized
+        if(response.status === 401 || resp?.code === "token_auth_failed"){
+            if(resp?.code === "token_auth_failed" && puter.env === 'web'){
+                try{
+                    puter.resetAuthToken();
+                    await puter.ui.authenticateWithPuter();
+                }catch(e){
+                    return reject_func({
+                        error: {
+                            code: 'auth_canceled', message: 'Authentication canceled'
+                        }
+                    })
+                }
+            }
+            // if error callback is provided, call it
+            if(error_cb && typeof error_cb === 'function')
+                error_cb({status: 401, message: 'Unauthorized'})
+            // reject promise
+            return reject_func({status: 401, message: 'Unauthorized'})
+        }
+        // HTTP Error - other
+        else if(response.status && response.status !== 200){
+            // if error callback is provided, call it
+            error_cb(resp)
+            // reject promise
+            return reject_func(resp)
+        }
+        // HTTP Success
+        else{
+            // Driver Error: permission denied
+            if(resp.success === false && resp.error?.code === 'permission_denied'){
+                let perm = await puter.ui.requestPermission({permission: 'driver:' + driverInterface + ':' + driverMethod});
+                // try sending again if permission was granted
+                if(perm.granted){
+                    // repeat request with permission granted
+                    return driverCall_(options, resolve_func, reject_func, driverInterface, driverMethod, driverArgs, method, contentType, settings);
+                }else{
+                    // if error callback is provided, call it
+                    error_cb(resp)
+                    // reject promise
+                    return reject_func(resp)
+                }
+            }
+            // Driver Error: other
+            else if(resp.success === false){
+                // if error callback is provided, call it
+                error_cb(resp)
+                // reject promise
+                return reject_func(resp)
+            }
+
+            let result = resp.result !== undefined ? resp.result : resp;
+            if ( settings.transform ) {
+                result = await settings.transform(result);
+            }
+
+            // Success: if callback is provided, call it
+            if(resolve_func.success)
+                success_cb(result);
+            // Success: resolve with the result
+            return resolve_func(result);
+        }
+    });
+
+    // error
+    xhr.addEventListener('error', function(e){
+        return handle_error(error_cb, reject_func, this);
+    })
+
+    // send request
+    xhr.send(JSON.stringify({
+        interface: driverInterface,
+        test_mode: settings?.test_mode, 
+        method: driverMethod,
+        args: driverArgs,
+    }));
+}
+
+class TeePromise {
+    static STATUS_PENDING = {};
+    static STATUS_RUNNING = {};
+    static STATUS_DONE = {};
+    constructor () {
+        this.status_ = this.constructor.STATUS_PENDING;
+        this.donePromise = new Promise((resolve, reject) => {
+            this.doneResolve = resolve;
+            this.doneReject = reject;
+        });
+    }
+    get status () {
+        return this.status_;
+    }
+    set status (status) {
+        this.status_ = status;
+        if ( status === this.constructor.STATUS_DONE ) {
+            this.doneResolve();
+        }
+    }
+    resolve (value) {
+        this.status_ = this.constructor.STATUS_DONE;
+        this.doneResolve(value);
+    }
+    awaitDone () {
+        return this.donePromise;
+    }
+    then (fn, rfn) {
+        return this.donePromise.then(fn, rfn);
+    }
+
+    reject (err) {
+        this.status_ = this.constructor.STATUS_DONE;
+        this.doneReject(err);
+    }
+    
+    /**
+     * @deprecated use then() instead
+     */
+    onComplete(fn) {
+        return this.then(fn);
+    }
+}
+
+async function blob_to_url (blob) {
+    const tp = new TeePromise();
+    const reader = new FileReader();
+    reader.onloadend = () => tp.resolve(reader.result);
+    reader.readAsDataURL(blob);
+    return await tp;
+}
+
+function blobToDataUri(blob) {
+    return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.onload = function(event) {
+            resolve(event.target.result);
+        };
+        reader.onerror = function(error) {
+            reject(error);
+        };
+        reader.readAsDataURL(blob);
+    });
+}
+
+function arrayBufferToDataUri(arrayBuffer) {
+    return new Promise((resolve, reject) => {
+        const blob = new Blob([arrayBuffer]);
+        const reader = new FileReader();
+        reader.onload = function(event) {
+            resolve(event.target.result);
+        };
+        reader.onerror = function(error) {
+            reject(error);
+        };
+        reader.readAsDataURL(blob);
+    });
+}
+
+export {parseResponse, uuidv4, handle_resp, handle_error, initXhr, setupXhrEventHandlers, driverCall,
+    TeePromise,
+    make_driver_method,
+    blob_to_url,
+    arrayBufferToDataUri,
+    blobToDataUri,
+};

+ 253 - 0
packages/puter-dot-js/src/modules/AI.js

@@ -0,0 +1,253 @@
+import * as utils from '../lib/utils.js'
+
+class AI{
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token and resets the socket connection with the updated token, if applicable.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [AI]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [AI]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+
+    img2txt = async (...args) => {
+        let MAX_INPUT_SIZE = 10 * 1024 * 1024;
+        let options = {};
+        let testMode = false;
+
+        // Check that the argument is not undefined or null
+        if(!args){
+            throw({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        // if argument is string transform it to the object that the API expects
+        if (typeof args[0] === 'string' || args[0] instanceof Blob) {
+            options.source = args[0];
+        }
+
+        // if input is a blob, transform it to a data URI
+        if (args[0].source instanceof Blob) {
+            options.source = await utils.blobToDataUri(args[0].source);
+        }
+
+        // check input size
+        if (options.source.length > this.MAX_INPUT_SIZE) {
+            throw { message: 'Input size cannot be larger than ' + MAX_INPUT_SIZE, code: 'input_too_large' };
+        }
+
+        // determine if test mode is enabled
+        if (typeof args[1] === 'boolean' && args[1] === true ||
+            typeof args[2] === 'boolean' && args[2] === true ||
+            typeof args[3] === 'boolean' && args[3] === true) {
+            testMode = true;
+        }
+    
+        console.log(args, options);
+        return await utils.make_driver_method(['source'], 'puter-ocr', 'recognize', {
+            test_mode: testMode ?? false,
+            transform: async (result) => {
+                let str = '';
+                for (let i = 0; i < result?.blocks?.length; i++) {
+                    if("text/textract:LINE" === result.blocks[i].type)
+                        str += result.blocks[i].text + "\n";
+                }
+                return str;
+            }
+        }).call(this, options);
+    }
+
+    txt2speech = async (...args) => {
+        let MAX_INPUT_SIZE = 3000;
+        let options = {};
+        let testMode = false;
+
+        if(!args){
+            throw({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        // if argument is string transform it to the object that the API expects
+        if (typeof args[0] === 'string') {
+            options = { text: args[0] };
+        }
+
+        // if second argument is string, it's the language
+        if (args[1] && typeof args[1] === 'string') {
+            options.language = args[1];
+        }
+
+        // check input size
+        if (options.text.length > this.MAX_INPUT_SIZE) {
+            throw { message: 'Input size cannot be larger than ' + MAX_INPUT_SIZE, code: 'input_too_large' };
+        }
+
+        // determine if test mode is enabled
+        if (typeof args[1] === 'boolean' && args[1] === true ||
+            typeof args[2] === 'boolean' && args[2] === true ||
+            typeof args[3] === 'boolean' && args[3] === true) {
+            testMode = true;
+        }
+    
+        return await utils.make_driver_method(['source'], 'puter-tts', 'synthesize', {
+            responseType: 'blob',
+            test_mode: testMode ?? false,
+            transform: async (result) => {
+                const url = await utils.blob_to_url(result);
+                const audio = new Audio(url);
+                audio.toString = () => url;
+                audio.valueOf = () => url;
+                return audio;
+            }
+        }).call(this, options);
+    }
+
+
+    // accepts either a string or an array of message objects
+    // if string, it's treated as the prompt which is a shorthand for { messages: [{ content: prompt }] }
+    // if object, it's treated as the full argument object that the API expects
+    chat = async (...args) => {
+        let options = {};
+        let testMode = false;
+
+        // Check that the argument is not undefined or null
+        if(!args){ 
+            throw({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        // ai.chat(prompt)
+        // ai.chat(prompt, testMode)
+        if (typeof args[0] === 'string' && (!args[1] || typeof args[1] === 'boolean')) {
+            options = { messages: [{ content: args[0] }] };
+        }
+        // ai.chat(prompt, imageURL/File)
+        // ai.chat(prompt, imageURL/File, testMode)
+        else if (typeof args[0] === 'string' && (typeof args[1] === 'string' || args[1] instanceof File)) {
+            // if imageURL is a File, transform it to a data URI
+            if(args[1] instanceof File){
+                args[1] = await utils.blobToDataUri(args[1]);
+            }
+
+            // parse args[1] as an image_url object
+            options = { 
+                vision: true,
+                messages: [
+                    { 
+                        content: [
+                            args[0],
+                            {
+                                image_url: {
+                                    url: args[1]
+                                }
+                            }
+                        ], 
+                    }
+                ]
+            };
+        }
+        // chat(prompt, [imageURLs])
+        else if (typeof args[0] === 'string' && Array.isArray(args[1])) {
+            // parse args[1] as an array of image_url objects
+            for (let i = 0; i < args[1].length; i++) {
+                args[1][i] = { image_url: { url: args[1][i] } };
+            }
+            options = { 
+                vision: true,
+                messages: [
+                    { 
+                        content: [
+                            args[0],
+                            ...args[1]
+                        ], 
+                    }
+                ]
+            };
+        }
+        // chat([messages])
+        else if (Array.isArray(args[0])) {
+            options = { messages: args[0] };
+        }
+
+        // determine if testMode is enabled
+        if (typeof args[1] === 'boolean' && args[1] === true ||
+            typeof args[2] === 'boolean' && args[2] === true ||
+            typeof args[3] === 'boolean' && args[3] === true) {
+            testMode = true;
+        }
+    
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['messages'], 'puter-chat-completion', 'complete', {
+            test_mode: testMode ?? false,
+            transform: async (result) => {
+                result.toString = () => {
+                    return result.message?.content;
+                };
+
+                result.valueOf = () => {
+                    return result.message?.content;
+                }
+
+                return result;
+            }
+        }).call(this, options);
+    }
+
+    txt2img = async (...args) => {
+        let options = {};
+        let testMode = false;
+
+        if(!args){
+            throw({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        // if argument is string transform it to the object that the API expects
+        if (typeof args[0] === 'string') {
+            options = { prompt: args[0] };
+        }
+
+        // if second argument is string, it's the `testMode`
+        if (typeof args[1] === 'boolean' && args[1] === true) {
+            testMode = true;
+        }
+    
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['prompt'], 'puter-image-generation', 'generate', {
+            responseType: 'blob',
+            test_mode: testMode ?? false,
+            transform: async blob => {
+                let img = new Image();
+                img.src = await utils.blob_to_url(blob);
+                img.toString = () => img.src;
+                img.valueOf = () => img.src;
+                return img;
+            }
+        }).call(this, options);
+    }
+}
+
+export default AI;

+ 158 - 0
packages/puter-dot-js/src/modules/Apps.js

@@ -0,0 +1,158 @@
+import * as utils from '../lib/utils.js'
+
+class Apps{
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [Apps]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [Apps]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+
+    list = async (...args) => {
+        let options = {};
+
+        options = { "predicate": ['user-can-edit'] };
+        
+        return utils.make_driver_method(['uid'], 'puter-apps', 'select').call(this, options);
+    }
+
+    create = async (...args) => {
+        let options = {};
+        // * allows for: puter.apps.new('example-app') *
+        if (typeof args[0] === 'string') {
+            let indexURL = args[1];
+            let title = args[2] ?? args[0];
+
+            options = { object: { name: args[0], index_url: indexURL, title: title}};
+        }else if (typeof args[0] === 'object' && args[0] !== null) {
+            let options_raw = args[0];
+            options = { 
+                object: { 
+                    name: options_raw.name, 
+                    index_url: options_raw.indexURL, 
+                    title: options_raw.title,
+                    description: options_raw.description,
+                    icon: options_raw.icon,
+                    maximize_on_start: options_raw.maximizeOnStart,
+                    filetype_associations: options_raw.filetypeAssociations,
+                }
+            };
+        }
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['object'], 'puter-apps', 'create').call(this, options);
+    }
+
+    update = async(...args) => {
+        let options = {};
+
+        // if there is one string argument, assume it is the app name
+        // * allows for: puter.apps.update('example-app') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            let object_raw = args[1];
+            let object = { 
+                name: object_raw.name, 
+                index_url: object_raw.indexURL, 
+                title: object_raw.title,
+                description: object_raw.description,
+                icon: object_raw.icon,
+                maximize_on_start: object_raw.maximizeOnStart,
+                filetype_associations: object_raw.filetypeAssociations,
+            };
+
+            options = { id: { name: args[0]}, object: object};
+        }
+
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['object'], 'puter-apps', 'update').call(this, options);
+    }
+
+    get = async(...args) => {
+        let options = {};
+        // if there is one string argument, assume it is the app name
+        // * allows for: puter.apps.get('example-app') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            options = { id: {name: args[0]}};
+        }
+        return utils.make_driver_method(['uid'], 'puter-apps', 'read').call(this, options);
+    }
+
+    delete = async(...args) => {
+        let options = {};
+        // if there is one string argument, assume it is the app name
+        // * allows for: puter.apps.get('example-app') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            options = { id: {name: args[0]}};
+        }
+        return utils.make_driver_method(['uid'], 'puter-apps', 'delete').call(this, options);
+    }
+
+    getDeveloperProfile = function(...args){
+        let options;
+
+        // If first argument is an object, it's the options
+        if (typeof args[0] === 'object' && args[0] !== null) {
+            options = args[0];
+        } else {
+            // Otherwise, we assume separate arguments are provided
+            options = {
+                success: args[0],
+                error: args[1],
+            };
+        }
+
+        return new Promise((resolve, reject) => {
+            let options;
+
+            // If first argument is an object, it's the options
+            if (typeof args[0] === 'object' && args[0] !== null) {
+                options = args[0];
+            } else {
+                // Otherwise, we assume separate arguments are provided
+                options = {
+                    success: args[0],
+                    error: args[1],
+                };
+            }
+    
+            return new Promise((resolve, reject) => {
+                const xhr = utils.initXhr('/get-dev-profile', puter.APIOrigin, puter.authToken, 'get');
+    
+                // set up event handlers for load and error events
+                utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+    
+                xhr.send();
+            })
+        });
+    }
+}
+
+export default Apps;

+ 115 - 0
packages/puter-dot-js/src/modules/Auth.js

@@ -0,0 +1,115 @@
+import * as utils from '../lib/utils.js'
+
+class Auth{
+    // Used to generate a unique message id for each message sent to the host environment
+    // we start from 1 because 0 is falsy and we want to avoid that for the message id
+    #messageID = 1;
+
+
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [Auth]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [Auth]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+    
+    signIn = () =>{
+        return new Promise((resolve, reject) => {
+            let msg_id = this.#messageID++;
+
+            let w = 600;
+            let h = 600;
+            let title = 'Puter';
+            var left = (screen.width/2)-(w/2);
+            var top = (screen.height/2)-(h/2);
+            window.open(puter.defaultGUIOrigin + '/action/sign-in?embedded_in_popup=true&msg_id=' + msg_id, 
+            title, 
+            'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
+
+            window.addEventListener('message', function(e){
+                if(e.data.msg_id == msg_id){
+                    // remove redundant attributes
+                    delete e.data.msg_id;
+                    delete e.data.msg;
+
+                    if(e.data.success){
+                        // set the auth token
+                        puter.setAuthToken(e.data.token);
+
+                        resolve(e.data);
+                    }else
+                        reject(e.data);
+
+                    // delete the listener
+                    window.removeEventListener('message', this);
+                }
+            });
+        });
+    }
+
+    isSignedIn = () =>{
+        if(puter.authToken)
+            return true;
+        else
+            return false;
+    }
+
+    getUser = function(...args){
+        let options;
+
+        // If first argument is an object, it's the options
+        if (typeof args[0] === 'object' && args[0] !== null) {
+            options = args[0];
+        } else {
+            // Otherwise, we assume separate arguments are provided
+            options = {
+                success: args[0],
+                error: args[1],
+            };
+        }
+
+        return new Promise((resolve, reject) => {
+            const xhr = utils.initXhr('/whoami', puter.APIOrigin, puter.authToken, 'get');
+
+            // set up event handlers for load and error events
+            utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+            xhr.send();
+        })
+    }
+
+    signOut = () =>{
+        puter.resetAuthToken();
+    }
+}
+
+export default Auth

+ 98 - 0
packages/puter-dot-js/src/modules/FSItem.js

@@ -0,0 +1,98 @@
+import path from "../lib/path.js"
+
+class FSItem{
+    constructor(options){
+        this.readURL        = options.readURL ?? options.read_url;
+        this.writeURL       = options.writeURL ?? options.write_url;
+        this.metadataURL    = options.metadataURL ?? options.metadata_url;
+        this.name           = options.name ?? options.fsentry_name;
+        this.uid            = options.uid ?? options.uuid ?? options.fsentry_uid ?? options.fsentry_id ?? options.fsentry_uuid ?? options.id;
+        this.id             = this.uid;
+        this.uuid           = this.uid;
+        this.path           = options.path ?? options.fsentry_path;
+        this.size           = options.size ?? options.fsentry_size;
+        this.accessed       = options.accessed ?? options.fsentry_accessed;
+        this.modified       = options.modified ?? options.fsentry_modified;
+        this.created        = options.created ?? options.fsentry_created;
+        this.isDirectory    = (options.isDirectory || options.is_dir || options.fsentry_is_dir) ? true : false;
+    }
+
+    write = async function(data){
+        return puter.fs.write( 
+            this.path,
+            new File([data], this.name), 
+            {
+                overwrite: true,
+                dedupeName: false,
+            },
+        );
+    }
+
+    // Watches for changes to the item, and calls the callback function
+    // with the new data when a change is detected.
+    watch = function(callback){
+        // todo - implement
+    }
+
+    open = function(callback){
+        // todo - implement
+    }
+
+    // Set wallpaper
+    setAsWallpaper = function(options, callback){
+        // todo - implement
+    }
+
+    rename = function(new_name){
+        return puter.fs.rename(this.uid, new_name);
+    }
+
+    move = function(dest_path, overwrite=false, new_name){
+        return puter.fs.move(this.path, dest_path, overwrite, new_name);
+    }
+
+    copy = function(destination_directory, auto_rename=false, overwrite=false){
+        return puter.fs.copy(this.path, destination_directory, auto_rename, overwrite);
+    }
+
+    delete = function(){
+        return puter.fs.delete(this.path);
+    }
+
+    versions = async function(){
+        // todo - implement
+    }
+
+    trash = function(){
+        // todo make sure puter allows for moving to trash by default
+        // todo implement trashing
+    }
+
+    mkdir = async function(name, auto_rename = false){
+        // Don't proceed if this is not a directory, throw error
+        if(!this.isDirectory)
+            throw new Error('mkdir() can only be called on a directory');    
+        
+        // mkdir
+        return puter.fs.mkdir(path.join(this.path, name));
+    }
+
+    metadata = async function(){
+        // todo - implement
+    }
+
+    readdir = async function(){
+        // Don't proceed if this is not a directory, throw error
+        if(!this.isDirectory)
+            throw new Error('readdir() can only be called on a directory');
+
+        // readdir
+        return puter.fs.readdir(this.path);
+    }
+
+    read = async function(){
+        return puter.fs.read(this.path);
+    }
+}
+
+export default FSItem;

+ 133 - 0
packages/puter-dot-js/src/modules/FileSystem/index.js

@@ -0,0 +1,133 @@
+import io from '../../lib/socket.io/socket.io.esm.min.js';
+
+// Operations
+import readdir from "./operations/readdir.js";
+import stat from "./operations/stat.js";
+import space from "./operations/space.js";
+import mkdir from "./operations/mkdir.js";
+import copy from "./operations/copy.js";
+import rename from "./operations/rename.js";
+import upload from "./operations/upload.js";
+import read from "./operations/read.js";
+import move from "./operations/move.js";
+import write from "./operations/write.js";
+import sign from "./operations/sign.js";
+// Why is this called deleteFSEntry instead of just delete? because delete is 
+// a reserved keyword in javascript
+import deleteFSEntry from "./operations/deleteFSEntry.js";
+
+class FileSystem{
+
+    readdir = readdir;
+    stat = stat;
+    space = space;
+    mkdir = mkdir;
+    copy = copy;
+    rename = rename;
+    upload = upload;
+    read = read;
+    // Why is this called deleteFSEntry instead of just delete? because delete is
+    // a reserved keyword in javascript.
+    delete = deleteFSEntry;
+    move = move;
+    write = write;
+    sign = sign;
+
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     * and connects to the socket.
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+        // Connect socket.
+        this.initializeSocket();
+    }
+
+    /**
+     * Initializes the socket connection to the server using the current API origin.
+     * If a socket connection already exists, it disconnects it before creating a new one.
+     * Sets up various event listeners on the socket to handle different socket events like
+     * connect, disconnect, reconnect, reconnect_attempt, reconnect_error, reconnect_failed, and error.
+     *
+     * @memberof FileSystem
+     * @returns {void}
+     */
+    initializeSocket() {
+        if (this.socket) {
+            this.socket.disconnect();
+        }
+
+        this.socket = io(this.APIOrigin, {
+            query: {
+                auth_token: this.authToken,
+            }
+        });
+
+        this.bindSocketEvents();
+    }
+
+    bindSocketEvents() {
+        this.socket.on('connect', () => {
+            console.log('FileSystem Socket: Connected', this.socket.id);
+        });
+
+        this.socket.on('disconnect', () => {
+            console.log('FileSystem Socket: Disconnected');
+        });
+
+        this.socket.on('reconnect', (attempt) => {
+            console.log('FileSystem Socket: Reconnected', this.socket.id);
+        });
+
+        this.socket.on('reconnect_attempt', (attempt) => {
+            console.log('FileSystem Socket: Reconnection Attemps', attempt);
+        });
+
+        this.socket.on('reconnect_error', (error) => {
+            console.log('FileSystem Socket: Reconnection Error', error);
+        });
+
+        this.socket.on('reconnect_failed', () => {
+            console.log('FileSystem Socket: Reconnection Failed');
+        });
+
+        this.socket.on('error', (error) => {
+            console.error('FileSystem Socket Error:', error);
+        });
+    }
+
+    /**
+     * Sets a new authentication token and resets the socket connection with the updated token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [FileSystem]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+        // reset socket
+        this.initializeSocket();
+    }
+
+    /**
+     * Sets the API origin and resets the socket connection with the updated API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [Apps]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+        // reset socket
+        this.initializeSocket();
+    }
+}
+
+export default FileSystem;

+ 61 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/copy.js

@@ -0,0 +1,61 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const copy = function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            source: args[0],
+            destination: args[1],
+            overwrite: args[2]?.overwrite,
+            new_name: args[2]?.newName || args[2]?.new_name,
+            create_missing_parents: args[2]?.createMissingParents || args[2]?.create_missing_parents,
+            new_metadata: args[2]?.newMetadata || args[2]?.new_metadata,
+            original_client_socket_id: args[2]?.excludeSocketID || args[2]?.original_client_socket_id,
+            success: args[3],
+            error: args[4],
+            // Add more if needed...
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // convert paths to absolute path
+        options.source = getAbsolutePathForApp(options.source);
+        options.destination = getAbsolutePathForApp(options.destination);
+
+        // create xhr object
+        const xhr = utils.initXhr('/copy', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        xhr.send(JSON.stringify({
+            original_client_socket_id: this.socket.id,
+            socket_id: this.socket.id,
+            source: options.source,
+            destination: options.destination,
+            overwrite: options.overwrite,
+            new_name: (options.new_name || options.newName),
+            // if user is copying an item to where its source is, change the name so there is no conflict
+            dedupe_name: (options.dedupe_name || options.dedupeName),
+        }));
+    })
+}
+
+export default copy;

+ 59 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/deleteFSEntry.js

@@ -0,0 +1,59 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+// why is this called deleteFSEntry instead of just delete? 
+// because delete is a reserved keyword in javascript
+const deleteFSEntry = async function(...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } 
+    // Otherwise, we assume separate arguments are provided
+    else {
+        options = {
+            paths: args[0],
+            recursive: args[1]?.recursive ?? true,
+            descendantsOnly: args[1]?.descendantsOnly ?? false,
+        };
+    }
+
+    // If paths is a string, convert to array
+    // this is to make it easier for the user to provide a single path without having to wrap it in an array
+    let paths = options.paths;
+    if(typeof paths === 'string')
+        paths = [paths];
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/delete', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        // convert paths to absolute paths
+        paths = paths.map((path) => {
+            return getAbsolutePathForApp(path);
+        })
+
+        xhr.send(JSON.stringify({
+            paths: paths,
+            descendants_only: (options.descendants_only || options.descendantsOnly) ?? false,
+            recursive: options.recursive ?? true,
+        }));
+    })
+}
+
+export default deleteFSEntry;

+ 59 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/mkdir.js

@@ -0,0 +1,59 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+import path from "../../../lib/path.js"
+
+const mkdir = function (...args) {
+    let options = {};
+
+    // If first argument is a string and the second is an object, or if the first is an object
+    if ((typeof args[0] === 'string' && typeof args[1] === 'object' && !(args[1] instanceof Function)) || (typeof args[0] === 'object' && args[0] !== null)) {
+        // If it's a string followed by an object, it means path then options
+        if (typeof args[0] === 'string') {
+            options.path = args[0];
+            // Merge the options
+            Object.assign(options, args[1]);
+            options.success = args[2];
+            options.error = args[3];
+        } else {
+            options = args[0];
+        }
+    } else if (typeof args[0] === 'string') {
+        // it means it's a path then functions (success and optionally error)
+        options.path = args[0];
+        options.success = args[1];
+        options.error = args[2];
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/mkdir', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        options.path = getAbsolutePathForApp(options.path);
+
+        xhr.send(JSON.stringify({
+            parent: path.dirname(options.path),
+            path:	path.basename(options.path), 
+            overwrite: options.overwrite ?? false,
+            dedupe_name: (options.rename || options.dedupeName) ?? false,
+            shortcut_to: options.shortcutTo,
+            original_client_socket_id: this.socket.id,
+            create_missing_parents: (options.recursive || options.createMissingParents) ?? false,
+        }));
+    })
+}
+
+export default mkdir;

+ 57 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/move.js

@@ -0,0 +1,57 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const move = function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            source: args[0],
+            destination: args[1],
+            overwrite: args[2]?.overwrite,
+            new_name: args[2]?.newName || args[2]?.new_name,
+            create_missing_parents: args[2]?.createMissingParents || args[2]?.create_missing_parents,
+            new_metadata: args[2]?.newMetadata || args[2]?.new_metadata,
+            original_client_socket_id: args[2]?.excludeSocketID || args[2]?.original_client_socket_id,
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // convert source and destination to absolute path
+        options.source = getAbsolutePathForApp(options.source);
+        options.destination = getAbsolutePathForApp(options.destination);
+
+        // create xhr object
+        const xhr = utils.initXhr('/move', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        xhr.send(JSON.stringify({
+            source: options.source,
+            destination: options.destination,
+            overwrite: options.overwrite,
+            new_name: (options.new_name || options.newName),
+            create_missing_parents: (options.create_missing_parents || options.createMissingParents),
+            new_metadata: (options.new_metadata || options.newMetadata),
+            original_client_socket_id: options.excludeSocketID,
+        }));
+    })
+}
+
+export default move;

+ 44 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/read.js

@@ -0,0 +1,44 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const read = function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            path: typeof args[0] === 'string' ? args[0] : (typeof args[0] === 'object' && args[0] !== null ? args[0].path : args[0]),
+            success: args[1],
+            error: args[2],
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // convert path to absolute path
+        options.path = getAbsolutePathForApp(options.path);
+
+        // create xhr object
+        const xhr = utils.initXhr('/read?file=' + encodeURIComponent(options.path), this.APIOrigin, this.authToken, 'get', "application/json;charset=UTF-8", 'blob');
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        xhr.send();
+    })
+}
+
+export default read;

+ 46 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/readdir.js

@@ -0,0 +1,46 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const readdir = async function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            path: args[0],
+            success: args[1],
+            error: args[2],
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // path is required
+        if(!options.path){
+            throw new Error({ code: 'NO_PATH', message: 'No path provided.' });
+        }
+
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/readdir', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        xhr.send(JSON.stringify({path: getAbsolutePathForApp(options.path)}));
+    })
+}
+
+export default readdir;

+ 56 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/rename.js

@@ -0,0 +1,56 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const rename = function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            path: args[0],
+            new_name: args[1],
+            success: args[2],
+            error: args[3],
+            // Add more if needed...
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/rename', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        let dataToSend = {
+            original_client_socket_id: options.excludeSocketID || options.original_client_socket_id,
+            new_name: options.new_name || options.newName,
+        };
+        
+        if (options.uid !== undefined) {
+            dataToSend.uid = options.uid;
+        } else if (options.path !== undefined) {
+            // If dirPath is not provided or it's not starting with a slash, it means it's a relative path
+            // in that case, we need to prepend the app's root directory to it
+            dataToSend.path = getAbsolutePathForApp(options.path);
+        }
+        
+        xhr.send(JSON.stringify(dataToSend));
+    })
+}
+
+export default rename;

+ 103 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/sign.js

@@ -0,0 +1,103 @@
+
+import * as utils from '../../../lib/utils.js';
+
+/**
+ * Signs a file system entry or entries and optionally calls a provided callback with the result.
+ * If a single item is passed, it is converted into an array.
+ * Sends a POST request to the server to sign the items.
+ *
+ * @param {(Object|Object[])} items - The file system entry or entries to be signed. Can be a single object or an array of objects.
+ * @param {function} [callback] - Optional callback function to be invoked with the result of the signing.
+ * @returns {(Object|Object[])} If a single item was passed, returns a single object. If multiple items were passed, returns an array of objects.
+ * @throws {Error} If the AJAX request fails.
+ * @async
+ */
+const sign = function(...args){
+    let options;
+
+    // Otherwise, we assume separate arguments are provided
+    options = {
+        app_uid: args[0],
+        items: args[1],
+        success: args[2],
+        error: args[3],
+        // Add more if needed...
+    };
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+
+        let items = options.items;
+
+        // if only a single item is passed, convert it to array
+        // so that the code below can work with arrays
+        if(!Array.isArray(items)){
+            items = [items]
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/sign', this.APIOrigin, this.authToken);
+
+        // response
+        xhr.addEventListener('load', async function(e){
+            const resp = await utils.parseResponse(this);
+            // error
+            if(this.status !== 200){
+                // if error callback is provided, call it
+                if(options.error && typeof options.error === 'function')
+                    options.error(resp)
+                // reject promise
+                return reject(resp)
+            }
+            // success
+            else{
+                let res = resp;
+                let result;
+                let token = res.token;
+                // if only a single item was passed, return a single object
+                if(items.length == 1){
+                    result = {...(res.signatures[0])};
+                }
+                // if multiple items were passed, return an array of objects
+                else{
+                    let obj=[];
+                    for(let i=0; i<res.signatures.length; i++){
+                        obj.push({...res.signatures[i]});
+                    }
+                    result = obj;
+                }
+
+                // if success callback is provided, call it
+                if(options.success && typeof options.success === 'function')
+                    options.success({token: token, items: result});
+                // resolve with success
+                return resolve({token: token, items: result});
+            }
+        });
+
+        xhr.upload.addEventListener('progress', function(e){
+        })
+
+        // error
+        xhr.addEventListener('error', function(e){
+            return utils.handle_error(options.error, reject, this);
+        })
+
+        xhr.send(JSON.stringify({
+            app_uid: options.app_uid,
+            items: items
+        }));
+    })
+}
+
+export default sign;

+ 40 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/space.js

@@ -0,0 +1,40 @@
+import * as utils from '../../../lib/utils.js';
+
+const space = function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            success: args[0],
+            error: args[1],
+            // Add more if needed...
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/df', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        xhr.send();
+    })
+}
+
+export default space;

+ 57 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/stat.js

@@ -0,0 +1,57 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const stat = async function (...args) {
+    let options;
+
+    // If first argument is an object, it's the options
+    if (typeof args[0] === 'object' && args[0] !== null) {
+        options = args[0];
+    } else {
+        // Otherwise, we assume separate arguments are provided
+        options = {
+            path: args[0],
+            options: typeof args[1] === 'object' ? args[1] : {},
+            success: typeof args[1] === 'object' ? args[2] : args[1],
+            error: typeof args[1] === 'object' ? args[3] : args[2],
+            // Add more if needed...
+        };
+    }
+
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject('Authentication failed.');
+            }
+        }
+
+        // create xhr object
+        const xhr = utils.initXhr('/stat', this.APIOrigin, this.authToken);
+
+        // set up event handlers for load and error events
+        utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+        let dataToSend = {};
+        if (options.uid !== undefined) {
+            dataToSend.uid = options.uid;
+        } else if (options.path !== undefined) {
+            // If dirPath is not provided or it's not starting with a slash, it means it's a relative path
+            // in that case, we need to prepend the app's root directory to it
+            dataToSend.path = getAbsolutePathForApp(options.path);
+        }
+        
+        dataToSend.return_subdomains = options.returnSubdomains;
+        dataToSend.return_permissions = options.returnPermissions;
+        dataToSend.return_versions = options.returnVersions;
+        dataToSend.return_size = options.returnSize;
+
+        xhr.send(JSON.stringify(dataToSend));
+    })
+}
+
+export default stat;

+ 419 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/upload.js

@@ -0,0 +1,419 @@
+import * as utils from '../../../lib/utils.js';
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+import path from "../../../lib/path.js"
+
+const upload = async function(items, dirPath, options = {}){
+    return new Promise(async (resolve, reject) => {
+        // If auth token is not provided and we are in the web environment, 
+        // try to authenticate with Puter
+        if(!puter.authToken && puter.env === 'web'){
+            try{
+                await puter.ui.authenticateWithPuter();
+            }catch(e){
+                // if authentication fails, throw an error
+                reject(e);
+            }
+        }
+
+        // xhr object to be used for the upload
+        let xhr = new XMLHttpRequest();
+
+        // Can not write to root
+        if(dirPath === '/'){
+            // if error callback is provided, call it
+            if(options.error && typeof options.error === 'function')
+                options.error('Can not upload to root directory.');
+            return reject('Can not upload to root directory.');
+        }
+
+        // If dirPath is not provided or it's not starting with a slash, it means it's a relative path
+        // in that case, we need to prepend the app's root directory to it
+        dirPath = getAbsolutePathForApp(dirPath);
+
+        // Generate a unique ID for this upload operation
+        // This will be used to uniquely identify this operation and its progress
+        // across servers and clients
+        const operation_id = utils.uuidv4();
+
+        // Call 'init' callback if provided
+        // init is basically a hook that allows the user to get the operation ID and the XMLHttpRequest object
+        if(options.init && typeof options.init === 'function'){
+            options.init(operation_id, xhr);
+        }
+
+        // keeps track of the amount of data uploaded to the server
+        let bytes_uploaded_to_server = 0;
+        // keeps track of the amount of data uploaded to the cloud
+        let bytes_uploaded_to_cloud = 0;
+
+        // This will hold the normalized entries to be uploaded
+        // Since 'items' could be a DataTransferItemList, FileList, File, or an array of any of these,
+        // we need to normalize it into an array of consistently formatted objects which will be held in 'entries'
+        let entries;
+
+        // will hold the total size of the upload
+        let total_size = 0;
+        let file_count = 0;
+
+        let seemsToBeParsedDataTransferItems = false;
+        if(Array.isArray(items) && items.length > 0){
+            for(let i=0; i<items.length; i++){
+                if(items[i] instanceof DataTransferItem || items[i] instanceof DataTransferItemList){
+                    seemsToBeParsedDataTransferItems = true;
+                }else{
+                    
+                }
+            }
+        }
+
+        // DataTransferItemList
+        if(items instanceof DataTransferItemList || items instanceof DataTransferItem  || items[0] instanceof DataTransferItem || options.parsedDataTransferItems){
+            // if parsedDataTransferItems is true, it means the user has already parsed the DataTransferItems
+            if(options.parsedDataTransferItems)
+                entries = items;
+            else
+                entries = await puter.ui.getEntriesFromDataTransferItems(items);
+
+            // Sort entries by size ascending
+            entries.sort((entry_a, entry_b) => {
+                if ( entry_a.isDirectory && ! entry_b.isDirectory ) return -1;
+                if ( ! entry_a.isDirectory && entry_b.isDirectory ) return 1;
+                if ( entry_a.isDirectory && entry_b.isDirectory ) return 0;
+        
+                return entry_a.size - entry_b.size;
+            });
+        }
+        // FileList/File
+        else if(items instanceof File || items[0] instanceof File || items instanceof FileList || items[0] instanceof FileList){
+            if(!Array.isArray(items))
+                entries = items instanceof FileList ? Array.from(items) : [items];
+            else
+                entries = items;
+
+            // Sort entries by size ascending
+            entries.sort((entry_a, entry_b) => {
+                return entry_a.size - entry_b.size;
+            })
+            // add FullPath property to each entry
+            for(let i=0; i<entries.length; i++){
+                entries[i].filepath = entries[i].name;
+                entries[i].fullPath = entries[i].name;
+            }
+        }
+        // blob
+        else if(items instanceof Blob){
+            // create a File object from the blob
+            let file = new File([items], options.name, { type: "text/plain" });
+            entries = [file];
+            // add FullPath property to each entry
+            for(let i=0; i<entries.length; i++){
+                entries[i].filepath = entries[i].name;
+                entries[i].fullPath = entries[i].name;
+            }
+        }
+        // String
+        else if(typeof items === 'string'){
+            // create a File object from the string
+            let file = new File([items], 'default.txt', { type: "text/plain" });
+            entries = [file];
+            // add FullPath property to each entry
+            for(let i=0; i<entries.length; i++){
+                entries[i].filepath = entries[i].name;
+                entries[i].fullPath = entries[i].name;
+            }
+        }       
+
+        // Will hold directories and files to be uploaded
+        let dirs = [];
+        let files = [];
+
+        // Separate files from directories
+        for(let i=0; i<entries.length; i++){
+            // skip empty entries
+            if(!entries[i])
+                continue;
+            //collect dirs
+            if(entries[i].isDirectory)
+                dirs.push({path: path.join(dirPath, entries[i].finalPath ? entries[i].finalPath : entries[i].fullPath)});
+            // also files
+            else
+                files.push(entries[i])
+            // stats about the upload to come
+            if(entries[i].size !== undefined){
+                total_size += (entries[i].size);
+                file_count++;
+            }
+        }
+
+        // Continue only if there are actually any files/directories to upload
+        if(dirs.length === 0 && files.length === 0){
+            if(options.error && typeof options.error === 'function'){
+                options.error({code: 'EMPTY_UPLOAD', message: 'No files or directories to upload.'});
+            }
+            return reject({code: 'EMPTY_UPLOAD', message: 'No files or directories to upload.'});
+        }
+
+        // Check storage capacity.
+        // We need to check the storage capacity before the upload starts because
+        // we want to avoid uploading files in case there is not enough storage space.
+        // If we didn't check before upload starts, we could end up in a scenario where
+        // the user uploads a very large folder/file and then the server rejects it because there is not enough space
+        //
+        // Space check in 'web' environment is currently not supported since it requires permissions.
+        let storage;
+        if(puter.env !== 'web'){
+            try{
+                storage = await this.space();
+                if(storage.capacity - storage.used < total_size){
+                    if(options.error && typeof options.error === 'function'){
+                        options.error({code: 'NOT_ENOUGH_SPACE', message: 'Not enough storage space available.'});
+                    }
+                    return reject({code: 'NOT_ENOUGH_SPACE', message: 'Not enough storage space available.'});
+                }
+            }catch(e){
+            }
+        }
+    
+        // total size of the upload is doubled because we will be uploading the files to the server
+        // and then the server will upload them to the cloud
+        total_size = total_size * 2;
+
+        // holds the data to be sent to the server
+        const fd = new FormData();
+    
+        //-------------------------------------------------
+        // Generate the requests to create all the 
+        // folders in this upload
+        //-------------------------------------------------
+        dirs.sort();
+        let mkdir_requests = [];
+    
+        for(let i=0; i < dirs.length; i++){
+            // update all file paths under this folder if dirname was changed
+            for(let j=0; j<files.length; j++){
+                // if file is in this folder and has not been processed yet
+                if(!files[j].puter_path_param && path.join(dirPath, files[j].filepath).startsWith((dirs[i].path) + '/')){
+                    files[j].puter_path_param = `$dir_${i}/`+ path.basename(files[j].filepath);
+                }
+            }
+    
+            // update all subdirs under this dir
+            for(let k=0; k < dirs.length; k++){
+                if(!dirs[k].puter_path_param && dirs[k].path.startsWith(dirs[i].path + '/')){
+                    dirs[k].puter_path_param = `$dir_${i}/`+ path.basename(dirs[k].path);
+                }
+            }
+        }
+
+        for(let i=0; i < dirs.length; i++){
+            let parent_path = path.dirname(dirs[i].puter_path_param || dirs[i].path);
+            let dir_path = dirs[i].puter_path_param || dirs[i].path;
+            
+            // remove parent path from the beginning of path since path is relative to parent
+            if(parent_path !== '/')
+                dir_path = dir_path.replace(parent_path, '');
+    
+            mkdir_requests.push({
+                op: 'mkdir',
+                parent: parent_path,
+                path: dir_path,
+                overwrite: options.overwrite ?? false,
+                dedupe_name: options.dedupeName ?? true,
+                create_missing_ancestors: options.createMissingAncestors ?? true,
+                as: `dir_${i}`,
+            });
+        }
+    
+        // inverse mkdir_requests so that the root folder is created first
+        // and then go down the tree
+        mkdir_requests.reverse();
+    
+        fd.append('operation_id', operation_id);
+        fd.append('socket_id', this.socket.id);
+        fd.append('original_client_socket_id', this.socket.id);
+
+        // Append mkdir operations to upload request
+        for(let i=0; i<mkdir_requests.length; i++){
+            fd.append('operation', JSON.stringify(mkdir_requests[i]));          
+        }
+    
+        // Append file metadata to upload request
+        if(!options.shortcutTo){
+            for(let i=0; i<files.length; i++){
+                fd.append('fileinfo', JSON.stringify({
+                    name: files[i].name,
+                    type: files[i].type,
+                    size: files[i].size,
+                }));
+            }
+        }
+        // Append write operations for each file
+        for(let i=0; i<files.length; i++){
+            fd.append('operation', JSON.stringify({
+                op: options.shortcutTo ? 'shortcut' : 'write',
+                dedupe_name: options.dedupeName ?? true,
+                overwrite: options.overwrite ?? false,
+                create_missing_ancestors: (options.createMissingAncestors ||  options.createMissingParents),
+                operation_id: operation_id,
+                path: (
+                    files[i].puter_path_param &&
+                    path.dirname(files[i].puter_path_param ?? '')
+                ) || (
+                    files[i].filepath &&
+                    path.join(dirPath, path.dirname(files[i].filepath))
+                ) || '',
+                name: path.basename(files[i].filepath),
+                item_upload_id: i,
+                shortcut_to: options.shortcutTo,
+                shortcut_to_uid: options.shortcutTo,
+                app_uid: options.appUID,
+            }));
+        }
+        
+        // Append files to upload
+        if(!options.shortcutTo){
+            for(let i=0; i<files.length; i++){
+                fd.append('file', files[i] ?? '');
+            }
+        }
+    
+        const progress_handler = (msg) => {
+            if(msg.operation_id === operation_id){
+                bytes_uploaded_to_cloud += msg.loaded_diff
+            }
+        }
+
+        // Handle upload progress events from server
+        this.socket.on('upload.progress', progress_handler);
+
+        // keeps track of the amount of data uploaded to the server
+        let previous_chunk_uploaded = null;
+    
+        // open request to server
+        xhr.open("post",(this.APIOrigin +'/batch'), true);
+        // set auth header
+        xhr.setRequestHeader("Authorization", "Bearer " + this.authToken);
+
+        // -----------------------------------------------
+        // Upload progress: client -> server
+        // -----------------------------------------------
+        xhr.upload.addEventListener('progress', function(e){
+            // update operation tracker
+            let chunk_uploaded;
+            if(previous_chunk_uploaded === null){
+                chunk_uploaded = e.loaded;
+                previous_chunk_uploaded = 0;
+            }else{
+                chunk_uploaded = e.loaded - previous_chunk_uploaded;
+            }
+            previous_chunk_uploaded += chunk_uploaded;
+            bytes_uploaded_to_server += chunk_uploaded;
+
+            // overall operation progress
+            let op_progress = ((bytes_uploaded_to_cloud + bytes_uploaded_to_server)/total_size * 100).toFixed(2);
+            op_progress = op_progress > 100 ? 100 : op_progress;
+
+            // progress callback function
+            if(options.progress && typeof options.progress === 'function')
+                options.progress(operation_id, op_progress);
+        })
+    
+        // -----------------------------------------------
+        // Upload progress: server -> cloud
+        // the following code will check the progress of the upload every 100ms
+        // -----------------------------------------------
+        let cloud_progress_check_interval = setInterval(function() {
+            // operation progress
+            let op_progress = ((bytes_uploaded_to_cloud + bytes_uploaded_to_server)/total_size * 100).toFixed(2);
+    
+            op_progress = op_progress > 100 ? 100 : op_progress;
+            if(options.progress && typeof options.progress === 'function')
+                options.progress(operation_id, op_progress);
+        }, 100);
+    
+        // -----------------------------------------------
+        // onabort
+        // -----------------------------------------------
+        xhr.onabort = ()=>{
+            // stop the cloud upload progress tracker
+            clearInterval(cloud_progress_check_interval);
+            // remove progress handler
+            this.socket.off('upload.progress', progress_handler);
+            // if an 'abort' callback is provided, call it
+            if(options.abort && typeof options.abort === 'function')
+                options.abort(operation_id);
+        }
+
+        // -----------------------------------------------
+        // on success/error
+        // -----------------------------------------------
+        xhr.onreadystatechange = async (e)=>{
+            if (xhr.readyState === 4) {
+                const resp = await utils.parseResponse(xhr);
+                // Error 
+                if((xhr.status >= 400 && xhr.status < 600) || (options.strict && xhr.status === 218)) {
+                    // stop the cloud upload progress tracker
+                    clearInterval(cloud_progress_check_interval);
+
+                    // remove progress handler
+                    this.socket.off('upload.progress', progress_handler);
+
+                    // If this is a 'strict' upload (i.e. status code is 218), we need to find out which operation failed 
+                    // and call the error callback with that operation.
+                    if(options.strict && xhr.status === 218){
+                        // find the operation that failed
+                        let failed_operation;
+                        for(let i=0; i<resp.results?.length; i++){
+                            if(resp.results[i].status !== 200){
+                                failed_operation = resp.results[i];
+                                break;
+                            }
+                        }
+                        // if error callback is provided, call it
+                        if(options.error && typeof options.error === 'function'){
+                            options.error(failed_operation);
+                        }
+                        return reject(failed_operation);
+                    }
+
+                    // if error callback is provided, call it
+                    if(options.error && typeof options.error === 'function'){
+                        options.error(resp);
+                    }
+                    return reject(resp);
+                }
+                // Success
+                else{
+                    if(!resp || !resp.results || resp.results.length === 0){
+                        // no results
+                        console.log('no results');
+                    }
+    
+                    let items = resp.results;
+                    items = items.length === 1 ? items[0] : items;
+
+                    // if success callback is provided, call it
+                    if(options.success && typeof options.success === 'function'){
+                        options.success(items);
+                    }
+                    // stop the cloud upload progress tracker
+                    clearInterval(cloud_progress_check_interval);
+                    // remove progress handler
+                    this.socket.off('upload.progress', progress_handler);
+
+                    return resolve(items);
+                }
+            }
+        }
+    
+        // Fire off the 'start' event
+        if(options.start && typeof options.start === 'function'){
+            options.start();
+        }
+
+        // send request
+        xhr.send(fd);
+    })
+}
+
+export default upload;

+ 53 - 0
packages/puter-dot-js/src/modules/FileSystem/operations/write.js

@@ -0,0 +1,53 @@
+import path from "../../../lib/path.js"
+import getAbsolutePathForApp from '../utils/getAbsolutePathForApp.js';
+
+const write = async function (targetPath, data, options = {}) {
+    // targetPath is required
+    if(!targetPath){
+        throw new Error({ code: 'NO_TARGET_PATH', message: 'No target path provided.' });
+    }
+    // if targetPath is a File
+    if(targetPath instanceof File && data === undefined){
+        data = targetPath;
+        targetPath = data.name;
+    }
+
+    // strict mode will cause the upload to fail if even one operation fails
+    // for example, if one of the files in a folder fails to upload, the entire upload will fail
+    // since write is a wrapper around upload to handle single-file uploads, we need to pass the strict option to upload
+    options.strict = true;
+
+    // by default, we want to overwrite existing files
+    options.overwrite = options.overwrite ?? true;
+
+    // if overwrite is true and dedupeName is not provided, set dedupeName to false
+    if(options.overwrite && options.dedupeName === undefined)
+        options.dedupeName = false;
+
+    // if targetPath is not provided or it's not starting with a slash, it means it's a relative path
+    // in that case, we need to prepend the app's root directory to it
+    targetPath = getAbsolutePathForApp(targetPath);
+
+    // extract file name from targetPath
+    const filename = path.basename(targetPath);
+
+    // extract the parent directory from targetPath
+    const parent = path.dirname(targetPath);
+
+    // if data is a string, convert it to a File object
+    if(typeof data === 'string'){
+        data = new File([data ?? ''], filename ?? 'Untitled.txt', { type: "text/plain" });
+    }
+    // blob
+    else if(data instanceof Blob){
+        data = new File([data ?? ''], filename ?? 'Untitled', { type: data.type });
+    }
+
+    if(!data)
+        data = new File([data ?? ''], filename);
+
+    // perform upload
+    return this.upload(data, parent, options);
+}
+
+export default write;

+ 21 - 0
packages/puter-dot-js/src/modules/FileSystem/utils/getAbsolutePathForApp.js

@@ -0,0 +1,21 @@
+import path from "../../../lib/path.js"
+
+const getAbsolutePathForApp = (relativePath)=>{
+    // if we are in the gui environment, return the relative path as is
+    if(puter.env === 'gui')
+        return relativePath;
+
+    // if no relative path is provided, use the current working directory
+    if(!relativePath)
+        relativePath = '.';
+
+    // If relativePath is not provided, or it's not starting with a slash or tilde,
+    // it means it's a relative path. In that case, prepend the app's root directory.
+    if (!relativePath || (!relativePath.startsWith('/') && !relativePath.startsWith('~') && puter.appID)) {
+        relativePath = path.join('~/AppData', puter.appID, relativePath);
+    }
+
+    return relativePath;
+}
+
+export default getAbsolutePathForApp;

+ 136 - 0
packages/puter-dot-js/src/modules/Hosting.js

@@ -0,0 +1,136 @@
+import * as utils from '../lib/utils.js';
+import getAbsolutePathForApp from './FileSystem/utils/getAbsolutePathForApp.js';
+
+class Hosting{
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [Router]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [Apps]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+
+    // todo document the `Subdomain` object.
+    list = utils.make_driver_method([], 'puter-subdomains', 'select');
+
+    create = async (...args) => {
+        let options = {};
+        // * allows for: puter.hosting.new('example-subdomain') *
+        if (typeof args[0] === 'string' && args.length === 1) {
+            // if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
+            // and use it as the subdomain. This is to make development easier.
+            if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
+                args[0] = args[0].split('.')[0];
+            }
+
+            options = { object: { subdomain: args[0] }};
+        }
+        // if there are two string arguments, assume they are the subdomain and the target directory
+        // * allows for: puter.hosting.new('example-subdomain', '/path/to/target') *
+        else if (Array.isArray(args) && args.length === 2 && typeof args[0] === 'string') {
+            // if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
+            // and use it as the subdomain. This is to make development easier.
+            if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
+                args[0] = args[0].split('.')[0];
+            }
+
+            // if the target directory is not an absolute path, make it an absolute path relative to the app's root directory
+            if(args[1]){
+                args[1] = getAbsolutePathForApp(args[1]);
+            }
+
+            options = { object: { subdomain: args[0], root_dir: args[1] }};
+        }
+        // allows for: puter.hosting.new({ subdomain: 'subdomain' })
+        else if (typeof args[0] === 'object') {
+            options = { object: args[0] };
+        }
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['object'], 'puter-subdomains', 'create').call(this, options);
+    }
+
+    update = async(...args) => {
+        let options = {};
+        // If there are two string arguments, assume they are the subdomain and the target directory
+        // * allows for: puter.hosting.update('example-subdomain', '/path/to/target') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            // if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
+            // and use it as the subdomain. This is to make development easier.
+            if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
+                args[0] = args[0].split('.')[0];
+            }
+
+            // if the target directory is not an absolute path, make it an absolute path relative to the app's root directory
+            if(args[1]){
+                args[1] = getAbsolutePathForApp(args[1]);
+            }
+
+            options = { id: {subdomain: args[0]}, object: { root_dir: args[1] ?? null }};
+        }
+
+        // Call the original chat.complete method
+        return await utils.make_driver_method(['object'], 'puter-subdomains', 'update').call(this, options);
+    }
+
+    get = async(...args) => {
+        let options = {};
+        // if there is one string argument, assume it is the subdomain
+        // * allows for: puter.hosting.get('example-subdomain') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            // if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
+            // and use it as the subdomain. This is to make development easier.
+            if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
+                args[0] = args[0].split('.')[0];
+            }
+
+            options = { id: {subdomain: args[0]}};
+        }
+        return utils.make_driver_method(['uid'], 'puter-subdomains', 'read').call(this, options);
+    }
+
+    delete = async(...args) => {
+        let options = {};
+        // if there is one string argument, assume it is the subdomain
+        // * allows for: puter.hosting.get('example-subdomain') *
+        if (Array.isArray(args) && typeof args[0] === 'string') {
+            // if subdomain is in the format of a `subdomain.puter.site` or `subdomain.puter.com`, extract the subdomain
+            // and use it as the subdomain. This is to make development easier.
+            if (args[0].match(/^[a-z0-9]+\.puter\.(site|com)$/)) {
+                args[0] = args[0].split('.')[0];
+            }
+
+            options = { id: {subdomain: args[0]}};
+        }
+        return utils.make_driver_method(['uid'], 'puter-subdomains', 'delete').call(this, options);
+    }
+}
+
+export default Hosting;

+ 205 - 0
packages/puter-dot-js/src/modules/KV.js

@@ -0,0 +1,205 @@
+import * as utils from '../lib/utils.js'
+
+class KV{
+    MAX_KEY_SIZE = 1024;
+    MAX_VALUE_SIZE = 400 * 1024;
+
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [KV]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [KV]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+
+    /**
+     * Resolves to 'true' on success, or rejects with an error on failure
+     * 
+     * `key` cannot be undefined or null.
+     * `key` size cannot be larger than 1mb.
+     * `value` size cannot be larger than 10mb.
+     */
+    set = utils.make_driver_method(['key', 'value'], 'puter-kvstore', 'set',{
+        preprocess: (args)=>{
+            // key cannot be undefined or null
+            if(args.key === undefined || args.key === null){
+                throw { message: 'Key cannot be undefined', code: 'key_undefined'};
+            }
+            // key size cannot be larger than MAX_KEY_SIZE
+            if(args.key.length > this.MAX_KEY_SIZE){
+                throw {message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'};
+            }
+            // value size cannot be larger than MAX_VALUE_SIZE
+            if(args.value && args.value.length > this.MAX_VALUE_SIZE){
+                throw {message: 'Value size cannot be larger than ' + this.MAX_VALUE_SIZE, code: 'value_too_large'};
+            }
+            return args;
+        }
+    })
+
+    /**
+     * Resolves to the value if the key exists, or `undefined` if the key does not exist. Rejects with an error on failure.
+     */
+    get = utils.make_driver_method(['key'], 'puter-kvstore', 'get', {
+        preprocess: (args)=>{
+            // key size cannot be larger than MAX_KEY_SIZE
+            if(args.key.length > this.MAX_KEY_SIZE){
+                throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
+            }
+
+            return args;
+        },
+        transform: (res)=>{
+            return res;
+        }
+    })
+
+    incr = async(...args) => {
+        let options = {};
+
+        // arguments are required
+        if(!args || args.length === 0){
+            throw ({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        options.key = args[0];
+        options.amount = args[1] ?? 1;
+
+        // key size cannot be larger than MAX_KEY_SIZE
+        if(options.key.length > this.MAX_KEY_SIZE){
+            throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
+        }
+
+        return utils.make_driver_method(['key'], 'puter-kvstore', 'incr').call(this, options);
+    }
+
+    decr = async(...args) => {
+        let options = {};
+
+        // arguments are required
+        if(!args || args.length === 0){
+            throw ({message: 'Arguments are required', code: 'arguments_required'});
+        }
+
+        options.key = args[0];
+        options.amount = args[1] ?? 1;
+
+        // key size cannot be larger than MAX_KEY_SIZE
+        if(options.key.length > this.MAX_KEY_SIZE){
+            throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
+        }
+
+        return utils.make_driver_method(['key'], 'puter-kvstore', 'decr').call(this, options);
+    }
+
+    // resolves to 'true' on success, or rejects with an error on failure
+    // will still resolve to 'true' if the key does not exist
+    del = utils.make_driver_method(['key'], 'puter-kvstore', 'del', {
+        preprocess: (args)=>{
+            // key size cannot be larger than this.MAX_KEY_SIZE
+            if(args.key.length > this.MAX_KEY_SIZE){
+                throw ({message: 'Key size cannot be larger than ' + this.MAX_KEY_SIZE, code: 'key_too_large'});
+            }
+
+            return args;
+        }
+    });
+
+    list = async(...args) => {
+        let options = {};
+        let pattern;
+        let returnValues = false;
+
+        // list(true) or list(pattern, true) will return the key-value pairs
+        if((args && args.length === 1 && args[0] === true) || (args && args.length === 2 && args[1] === true)){
+            options = {};
+            returnValues = true;
+        }
+        // return only the keys, default behavior
+        else{
+            options = { as: 'keys'};
+        }
+
+        // list(pattern)
+        // list(pattern, true)
+        if((args && args.length === 1 && typeof args[0] === 'string') || (args && args.length === 2 && typeof args[0] === 'string' && args[1] === true)){
+            pattern = args[0];
+        }
+
+        return utils.make_driver_method([], 'puter-kvstore', 'list', {
+            transform: (res)=>{
+                // glob pattern was provided
+                if(pattern){
+                    // consider both the key and the value 
+                    if(!returnValues) {                  
+                        let keys = res.filter((key)=>{
+                            return globMatch(pattern, key);
+                        });
+                        return keys;
+                    }else{
+                        let keys = res.filter((key_value_pair)=>{
+                            return globMatch(pattern, key_value_pair.key);
+                        });
+                        return keys;
+                    }
+                }
+
+                return res;
+            }
+        }).call(this, options);
+    }
+
+    // resolve to 'true' on success, or rejects with an error on failure
+    // will still resolve to 'true' if there are no keys
+    flush = utils.make_driver_method([], 'puter-kvstore', 'flush')
+
+    // clear is an alias for flush
+    clear = this.flush;
+}
+
+
+function globMatch(pattern, str) {
+    const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+    let regexPattern = escapeRegExp(pattern)
+        .replace(/\\\*/g, '.*') // Replace * with .*
+        .replace(/\\\?/g, '.') // Replace ? with .
+        .replace(/\\\[/g, '[') // Replace [ with [
+        .replace(/\\\]/g, ']') // Replace ] with ]
+        .replace(/\\\^/g, '^'); // Replace ^ with ^
+
+    let re = new RegExp('^' + regexPattern + '$');
+    return re.test(str);
+}
+
+
+
+export default KV

+ 90 - 0
packages/puter-dot-js/src/modules/OS.js

@@ -0,0 +1,90 @@
+import * as utils from '../lib/utils.js'
+
+class OS{
+    /**
+     * Creates a new instance with the given authentication token, API origin, and app ID,
+     *
+     * @class
+     * @param {string} authToken - Token used to authenticate the user.
+     * @param {string} APIOrigin - Origin of the API server. Used to build the API endpoint URLs.
+     * @param {string} appID - ID of the app to use.
+     */
+    constructor (authToken, APIOrigin, appID) {
+        this.authToken = authToken;
+        this.APIOrigin = APIOrigin;
+        this.appID = appID;
+    }
+
+    /**
+     * Sets a new authentication token.
+     *
+     * @param {string} authToken - The new authentication token.
+     * @memberof [OS]
+     * @returns {void}
+     */
+    setAuthToken (authToken) {
+        this.authToken = authToken;
+    }
+
+    /**
+     * Sets the API origin.
+     * 
+     * @param {string} APIOrigin - The new API origin.
+     * @memberof [Apps]
+     * @returns {void}
+     */
+    setAPIOrigin (APIOrigin) {
+        this.APIOrigin = APIOrigin;
+    }
+
+    user = function(...args){
+        let options;
+
+        // If first argument is an object, it's the options
+        if (typeof args[0] === 'object' && args[0] !== null) {
+            options = args[0];
+        } else {
+            // Otherwise, we assume separate arguments are provided
+            options = {
+                success: args[0],
+                error: args[1],
+            };
+        }
+
+        return new Promise((resolve, reject) => {
+            const xhr = utils.initXhr('/whoami', this.APIOrigin, this.authToken, 'get');
+
+            // set up event handlers for load and error events
+            utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+            xhr.send();
+        })
+    }
+
+    version = function(...args){
+        let options;
+
+        // If first argument is an object, it's the options
+        if (typeof args[0] === 'object' && args[0] !== null) {
+            options = args[0];
+        } else {
+            // Otherwise, we assume separate arguments are provided
+            options = {
+                success: args[0],
+                error: args[1],
+                // Add more if needed...
+            };
+        }
+
+        return new Promise((resolve, reject) => {
+            const xhr = utils.initXhr('/version', this.APIOrigin, this.authToken, 'get');
+
+            // set up event handlers for load and error events
+            utils.setupXhrEventHandlers(xhr, options.success, options.error, resolve, reject);
+
+            xhr.send();
+        })
+    }
+}
+
+export default OS

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 27 - 0
packages/puter-dot-js/src/modules/PuterDialog.js


+ 1019 - 0
packages/puter-dot-js/src/modules/UI.js

@@ -0,0 +1,1019 @@
+import FSItem from './FSItem.js';
+import PuterDialog from './PuterDialog.js';
+import EventListener  from '../lib/EventListener.js';
+
+// AppConnection provides an API for interacting with another app.
+// It's returned by UI methods, and cannot be constructed directly by user code.
+// For basic usage:
+// - postMessage(message)        Send a message to the target app
+// - on('message', callback)     Listen to messages from the target app
+class AppConnection extends EventListener {
+    // targetOrigin for postMessage() calls to Puter
+    #puterOrigin = '*';
+
+    // Whether the target app is open
+    #isOpen;
+
+    constructor(messageTarget, appInstanceID, targetAppInstanceID) {
+        super([
+            'message', // The target sent us something with postMessage()
+            'close',   // The target app was closed
+        ]);
+        this.messageTarget = messageTarget;
+        this.appInstanceID = appInstanceID;
+        this.targetAppInstanceID = targetAppInstanceID;
+        this.#isOpen = true;
+
+        // TODO: Set this.#puterOrigin to the puter origin
+
+        window.addEventListener('message', event => {
+            if (event.data.msg === 'messageToApp') {
+                if (event.data.appInstanceID !== this.targetAppInstanceID) {
+                    // Message is from a different AppConnection; ignore it.
+                    return;
+                }
+                if (event.data.targetAppInstanceID !== this.appInstanceID) {
+                    console.error(`AppConnection received message intended for wrong app! appInstanceID=${this.appInstanceID}, target=${event.data.targetAppInstanceID}`);
+                    return;
+                }
+                this.emit('message', event.data.contents);
+                return;
+            }
+
+            if (event.data.msg === 'appClosed') {
+                if (event.data.appInstanceID !== this.targetAppInstanceID) {
+                    // Message is from a different AppConnection; ignore it.
+                    return;
+                }
+
+                this.#isOpen = false;
+                this.emit('close', {
+                    appInstanceID: this.targetAppInstanceID,
+                });
+            }
+        });
+    }
+
+    postMessage(message) {
+        if (!this.#isOpen) {
+            console.warn('Trying to post message on a closed AppConnection');
+            return;
+        }
+
+        this.messageTarget.postMessage({
+            msg: 'messageToApp',
+            appInstanceID: this.appInstanceID,
+            targetAppInstanceID: this.targetAppInstanceID,
+            targetAppOrigin: '*', // TODO: Specify this somehow
+            contents: message,
+        }, this.#puterOrigin);
+    }
+
+    // Attempt to close the target application
+    close() {
+        if (!this.#isOpen) {
+            console.warn('Trying to close an app on a closed AppConnection');
+            return;
+        }
+
+        this.messageTarget.postMessage({
+            msg: 'closeApp',
+            appInstanceID: this.appInstanceID,
+            targetAppInstanceID: this.targetAppInstanceID,
+        }, this.#puterOrigin);
+    }
+}
+
+class UI extends EventListener {
+    // Used to generate a unique message id for each message sent to the host environment
+    // we start from 1 because 0 is falsy and we want to avoid that for the message id
+    #messageID = 1;
+
+    // Holds the callback functions for the various events 
+    // that are triggered when a watched item has changed.
+    itemWatchCallbackFunctions = [];
+
+    // Holds the unique app instance ID that is provided by the host environment
+    appInstanceID;
+
+    // Holds the unique app instance ID for the parent (if any), which is provided by the host environment
+    parentInstanceID;
+
+    // If we have a parent app, holds an AppConnection to it
+    #parentAppConnection = null;
+
+    // Holds the callback functions for the various events 
+    // that can be triggered by the host environment's messages.
+    #callbackFunctions = [];
+
+    // onWindowClose() is executed right before the window is closed. Users can override this function 
+    // to perform a variety of tasks right before window is closed. Users can override this function.
+    #onWindowClose;
+
+    // When an item is opened by this app in any way onItemsOpened() is executed. Users can override this function. 
+    #onItemsOpened;
+
+    #onLaunchedWithItems;
+
+    // List of events that can be listened to.
+    #eventNames;
+
+    // The most recent value that we received for a given broadcast, by name.
+    #lastBroadcastValue = new Map(); // name -> data
+
+    // Replaces boilerplate for most methods: posts a message to the GUI with a unique ID, and sets a callback for it.
+    #postMessageWithCallback = function(name, resolve, args = {}) {
+        const msg_id = this.#messageID++;
+        this.messageTarget?.postMessage({
+            msg: name,
+            env: this.env,
+            appInstanceID: this.appInstanceID,
+            uuid: msg_id,
+            ...args,
+        }, '*');
+        //register callback
+        this.#callbackFunctions[msg_id] = resolve;
+    }
+
+    constructor (appInstanceID, parentInstanceID, appID, env) {
+        const eventNames = [
+            'localeChanged',
+            'themeChanged',
+        ];
+        super(eventNames);
+        this.#eventNames = eventNames;
+        this.appInstanceID = appInstanceID;
+        this.parentInstanceID = parentInstanceID;
+        this.appID = appID;
+        this.env = env;
+
+        if(this.env === 'app'){
+            this.messageTarget = window.parent;
+        }
+        else if(this.env === 'gui'){
+            return;
+        }
+
+        if (this.parentInstanceID) {
+            this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID);
+        }
+
+        // Tell the host environment that this app is using the Puter SDK and is ready to receive messages,
+        // this will allow the OS to send custom messages to the app
+        this.messageTarget?.postMessage({
+            msg: "READY",
+            appInstanceID: this.appInstanceID,
+        }, '*');
+
+        // When this app's window is focused send a message to the host environment
+        window.addEventListener('focus', (e) => {
+            this.messageTarget?.postMessage({
+                msg: "windowFocused",
+                appInstanceID: this.appInstanceID,
+            }, '*');
+        });
+
+        // Bind the message event listener to the window
+        let lastDraggedOverElement = null;
+        window.addEventListener('message', async (e) => {
+            // `error`
+            if(e.data.error){
+                throw e.data.error;
+            }
+            // `focus` event
+            else if(e.data.msg && e.data.msg === 'focus'){
+                window.focus();
+            }
+            // `click` event
+            else if(e.data.msg && e.data.msg === 'click'){
+                // Get the element that was clicked on and click it
+                const clicked_el = document.elementFromPoint(e.data.x, e.data.y);
+                if(clicked_el !== null)
+                    clicked_el.click();
+            }
+            // `dragover` event based on the `drag` event from the host environment
+            else if(e.data.msg && e.data.msg === 'drag'){
+                // Get the element being dragged over
+                const draggedOverElement = document.elementFromPoint(e.data.x, e.data.y);
+                if(draggedOverElement !== lastDraggedOverElement){
+                    // If the last element exists and is different from the current, dispatch a dragleave on it
+                    if(lastDraggedOverElement){
+                        const dragLeaveEvent = new Event('dragleave', {
+                            bubbles: true,
+                            cancelable: true,
+                            clientX: e.data.x,
+                            clientY: e.data.y
+                        });
+                        lastDraggedOverElement.dispatchEvent(dragLeaveEvent);
+                    }
+                    // If the current element exists and is different from the last, dispatch dragenter on it
+                    if(draggedOverElement){
+                        const dragEnterEvent = new Event('dragenter', {
+                            bubbles: true,
+                            cancelable: true,
+                            clientX: e.data.x,
+                            clientY: e.data.y
+                        });
+                        draggedOverElement.dispatchEvent(dragEnterEvent);
+                    }
+
+                    // Update the lastDraggedOverElement
+                    lastDraggedOverElement = draggedOverElement;
+                }
+            }
+            // `drop` event
+            else if(e.data.msg && e.data.msg === 'drop'){
+                if(lastDraggedOverElement){
+                    const dropEvent = new CustomEvent('drop', {
+                        bubbles: true,
+                        cancelable: true,
+                        detail: {
+                            clientX: e.data.x,
+                            clientY: e.data.y,
+                            items: e.data.items
+                        }
+                    });
+                    lastDraggedOverElement.dispatchEvent(dropEvent);
+                    
+                    // Reset the lastDraggedOverElement
+                    lastDraggedOverElement = null;
+                }
+            }
+            // windowWillClose
+            else if(e.data.msg === 'windowWillClose'){
+                // If the user has not overridden onWindowClose() then send a message back to the host environment
+                // to let it know that it is ok to close the window.
+                if(this.#onWindowClose === undefined){
+                    this.messageTarget?.postMessage({
+                        msg: true,
+                        appInstanceID: this.appInstanceID,
+                        original_msg_id: e.data.msg_id,
+                    }, '*');
+                }
+                // If the user has overridden onWindowClose() then send a message back to the host environment
+                // to let it know that it is NOT ok to close the window. Then execute onWindowClose() and the user will 
+                // have to manually close the window.
+                else{
+                    this.messageTarget?.postMessage({
+                        msg: false,
+                        appInstanceID: this.appInstanceID,
+                        original_msg_id: e.data.msg_id,
+                    }, '*');
+                    this.#onWindowClose();
+                }
+            }
+            // itemsOpened
+            else if(e.data.msg === 'itemsOpened'){
+                // If the user has not overridden onItemsOpened() then only send a message back to the host environment
+                if(this.#onItemsOpened === undefined){
+                    this.messageTarget?.postMessage({
+                        msg: true,
+                        appInstanceID: this.appInstanceID,
+                        original_msg_id: e.data.msg_id,
+                    }, '*');        
+                }
+                // If the user has overridden onItemsOpened() then send a message back to the host environment
+                // and execute onItemsOpened()
+                else{
+                    this.messageTarget?.postMessage({
+                        msg: false,
+                        appInstanceID: this.appInstanceID,
+                        original_msg_id: e.data.msg_id,
+                    }, '*');
+
+                    let items = [];
+                    if(e.data.items.length > 0){
+                        for (let index = 0; index < e.data.items.length; index++)
+                            items.push(new FSItem(e.data.items[index]))
+                    }
+                    this.#onItemsOpened(items);
+                }
+            }
+            // getAppDataSucceeded
+            else if(e.data.msg === 'getAppDataSucceeded'){
+                let appDataItem = new FSItem(e.data.item);
+                if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
+                    this.#callbackFunctions[e.data.original_msg_id](appDataItem);
+                }
+            }
+            // readAppDataFileSucceeded
+            else if(e.data.msg === 'readAppDataFileSucceeded'){
+                let appDataItem = new FSItem(e.data.item);
+                if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
+                    this.#callbackFunctions[e.data.original_msg_id](appDataItem);
+                }
+            }
+            // readAppDataFileFailed
+            else if(e.data.msg === 'readAppDataFileFailed'){
+                if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
+                    this.#callbackFunctions[e.data.original_msg_id](null);
+                }
+            }
+            // Determine if this is a response to a previous message and if so, is there
+            // a callback function for this message? if answer is yes to both then execute the callback
+            else if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){
+                if(e.data.msg === 'fileOpenPicked'){
+                    // 1 item returned
+                    if(e.data.items.length === 1){
+                        this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.items[0]));                             
+                    }
+                    // multiple items returned
+                    else if(e.data.items.length > 1){
+                        // multiple items returned
+                        let items = [];
+                        for (let index = 0; index < e.data.items.length; index++)
+                            items.push(new FSItem(e.data.items[index]))
+                        this.#callbackFunctions[e.data.original_msg_id](items);
+                    }
+                }
+                else if(e.data.msg === 'directoryPicked'){
+                    // 1 item returned
+                    if(e.data.items.length === 1){
+                        this.#callbackFunctions[e.data.original_msg_id](new FSItem({
+                            uid: e.data.items[0].uid,
+                            name: e.data.items[0].fsentry_name,
+                            path: e.data.items[0].path,
+                            readURL: e.data.items[0].read_url,
+                            writeURL: e.data.items[0].write_url,
+                            metadataURL: e.data.items[0].metadata_url,
+                            isDirectory: true,
+                            size: e.data.items[0].fsentry_size,
+                            accessed: e.data.items[0].fsentry_accessed,
+                            modified: e.data.items[0].fsentry_modified,
+                            created: e.data.items[0].fsentry_created,
+                        }));
+                    }
+                    // multiple items returned
+                    else if(e.data.items.length > 1){
+                        // multiple items returned
+                        let items = [];
+                        for (let index = 0; index < e.data.items.length; index++)
+                            items.push(new FSItem(e.data.items[index]))
+                        this.#callbackFunctions[e.data.original_msg_id](items);
+                    }
+                }
+                else if(e.data.msg === 'colorPicked'){
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](e.data.color);
+                }
+                else if(e.data.msg === 'fontPicked'){
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](e.data.font); 
+                }
+                else if(e.data.msg === 'alertResponded'){
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](e.data.response); 
+                }
+                else if(e.data.msg === 'promptResponded'){
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](e.data.response); 
+                }
+                else if(e.data.msg === "fileSaved"){
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file)); 
+                }
+                else if (e.data.msg === 'childAppLaunched') {
+                    // execute callback with a new AppConnection to the child
+                    const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id);
+                    this.#callbackFunctions[e.data.original_msg_id](connection);
+                }
+                else{
+                    // execute callback
+                    this.#callbackFunctions[e.data.original_msg_id](e.data);
+                }
+
+                //remove this callback function since it won't be needed again
+                delete this.#callbackFunctions[e.data.original_msg_id];
+            }
+            // Item Watch response
+            else if(e.data.msg === "itemChanged" && e.data.data && e.data.data.uid){
+                //excute callback
+                if(itemWatchCallbackFunctions[e.data.data.uid] && typeof itemWatchCallbackFunctions[e.data.data.uid] === 'function')
+                    itemWatchCallbackFunctions[e.data.data.uid](e.data.data);
+            }
+            // Broadcasts
+            else if (e.data.msg === 'broadcast') {
+                const { name, data } = e.data;
+                if (!this.#eventNames.includes(name)) {
+                    return;
+                }
+                this.emit(name, data);
+                this.#lastBroadcastValue.set(name, data);
+            }
+        });
+    }
+
+    onWindowClose = function(callback) {
+        this.#onWindowClose = callback;
+    }
+
+    onItemsOpened = function(callback) {
+        // DEPRECATED - this is also called when items are dropped on the app, which in new versions should be handled
+        // with the 'drop' event.
+        // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
+        // Even though the file has been opened when the app is launched, we need to wait for the onItemsOpened callback to be set
+        // before we can call it. This is why we need to check the URL parameters here.
+        // This should also be done only the very first time the callback is set (hence the if(!this.#onItemsOpened) check) since
+        // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
+        if(!this.#onItemsOpened){
+            let URLParams = new URLSearchParams(window.location.search);
+            if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
+                let fpath = URLParams.get('puter.item.path');
+                fpath = `~/` + fpath.split('/').slice(2).join('/');
+                callback([new FSItem({
+                    name: URLParams.get('puter.item.name'),
+                    path: fpath,
+                    uid: URLParams.get('puter.item.uid'),
+                    readURL: URLParams.get('puter.item.read_url'),
+                    writeURL: URLParams.get('puter.item.write_url'),
+                    metadataURL: URLParams.get('puter.item.metadata_url'),
+                    size: URLParams.get('puter.item.size'),
+                    accessed: URLParams.get('puter.item.accessed'),
+                    modified: URLParams.get('puter.item.modified'),
+                    created: URLParams.get('puter.item.created'),
+                })]);
+            }
+        }
+
+        this.#onItemsOpened = callback;
+    }
+
+    onLaunchedWithItems = function(callback) {
+        // Check if a file was opened with this app, i.e. check URL parameters of window/iframe
+        // Even though the file has been opened when the app is launched, we need to wait for the onLaunchedWithItems callback to be set
+        // before we can call it. This is why we need to check the URL parameters here.
+        // This should also be done only the very first time the callback is set (hence the if(!this.#onLaunchedWithItems) check) since
+        // the URL parameters will be checked every time the callback is set which can cause problems if the callback is set multiple times.
+        if(!this.#onLaunchedWithItems){
+            let URLParams = new URLSearchParams(window.location.search);
+            if(URLParams.has('puter.item.name') && URLParams.has('puter.item.uid') && URLParams.has('puter.item.read_url')){
+                let fpath = URLParams.get('puter.item.path');
+                fpath = `~/` + fpath.split('/').slice(2).join('/');
+                callback([new FSItem({
+                    name: URLParams.get('puter.item.name'),
+                    path: fpath,
+                    uid: URLParams.get('puter.item.uid'),
+                    readURL: URLParams.get('puter.item.read_url'),
+                    writeURL: URLParams.get('puter.item.write_url'),
+                    metadataURL: URLParams.get('puter.item.metadata_url'),
+                    size: URLParams.get('puter.item.size'),
+                    accessed: URLParams.get('puter.item.accessed'),
+                    modified: URLParams.get('puter.item.modified'),
+                    created: URLParams.get('puter.item.created'),
+                })]);
+            }
+        }
+
+        this.#onLaunchedWithItems = callback;
+    }
+
+    alert = function(message, buttons, options, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('ALERT', resolve, { message, buttons, options });
+        })
+    }
+
+    prompt = function(message, placeholder, options, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('PROMPT', resolve, { message, placeholder, options });
+        })
+    }
+
+    showDirectoryPicker = function(options, callback){
+        return new Promise((resolve) => {
+            const msg_id = this.#messageID++;
+            if(this.env === 'app'){
+                this.messageTarget?.postMessage({
+                    msg: "showDirectoryPicker",
+                    appInstanceID: this.appInstanceID,
+                    uuid: msg_id,
+                    options: options,
+                    env: this.env,
+                }, '*');
+            }else{
+                let w = 700;
+                let h = 400;
+                let title = 'Puter: Open Directory';
+                var left = (screen.width/2)-(w/2);
+                var top = (screen.height/2)-(h/2);
+                window.open(`${puter.defaultGUIOrigin}/action/show-directory-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&options=${JSON.stringify(options)}`, 
+                title, 
+                'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
+            }
+
+            //register callback
+            this.#callbackFunctions[msg_id] = resolve;
+        })
+    }
+
+    showOpenFilePicker = function(options, callback){
+        return new Promise((resolve) => {
+            const msg_id = this.#messageID++;
+
+            if(this.env === 'app'){
+                this.messageTarget?.postMessage({
+                    msg: "showOpenFilePicker",
+                    appInstanceID: this.appInstanceID,
+                    uuid: msg_id,
+                    options: options ?? {},
+                    env: this.env,
+                }, '*');
+            }else{                
+                let w = 700;
+                let h = 400;
+                let title = 'Puter: Open File';
+                var left = (screen.width/2)-(w/2);
+                var top = (screen.height/2)-(h/2);
+                window.open(`${puter.defaultGUIOrigin}/action/show-open-file-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&options=${JSON.stringify(options ?? {})}`, 
+                title, 
+                'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
+            }
+            //register callback
+            this.#callbackFunctions[msg_id] = resolve;
+        })
+    }
+
+    showFontPicker = function(options){
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('showFontPicker', resolve, { options: options ?? {} });
+        })
+    }
+
+    showColorPicker = function(options){
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('showColorPicker', resolve, { options: options ?? {} });
+        })
+    }
+
+    showSaveFilePicker = function(content, suggestedName){
+        return new Promise((resolve) => {
+            const msg_id = this.#messageID++;
+            const url = (Object.prototype.toString.call(content) === '[object URL]' ? content : undefined);
+
+            if(this.env === 'app'){
+                this.messageTarget?.postMessage({
+                    msg: "showSaveFilePicker",
+                    appInstanceID: this.appInstanceID,
+                    content: url ? undefined : content,
+                    url: url ? url.toString() : undefined,
+                    suggestedName: suggestedName ?? '',
+                    env: this.env,
+                    uuid: msg_id
+                }, '*');
+            }else{
+                window.addEventListener('message', async (e) => {
+                    if(e.data?.msg === "sendMeFileData"){
+                        // Send the blob URL to the host environment
+                        e.source.postMessage({
+                            msg: "showSaveFilePickerPopup",
+                            content: url ? undefined : content,
+                            url: url ? url.toString() : undefined,
+                            suggestedName: suggestedName ?? '',
+                            env: this.env,
+                            uuid: msg_id
+                        }, '*');
+
+                        // remove the event listener
+                        window.removeEventListener('message', this);
+                    }
+                });
+                // Create a Blob from your binary data
+                let blob = new Blob([content], {type: 'application/octet-stream'});
+
+                // Create an object URL for the Blob
+                let objectUrl = URL.createObjectURL(blob);
+
+                let w = 700;
+                let h = 400;
+                let title = 'Puter: Save File';
+                var left = (screen.width/2)-(w/2);
+                var top = (screen.height/2)-(h/2);
+                window.open(`${puter.defaultGUIOrigin}/action/show-save-file-picker?embedded_in_popup=true&msg_id=${msg_id}&appInstanceID=${this.appInstanceID}&env=${this.env}&blobUrl=${encodeURIComponent(objectUrl)}`, 
+                title, 
+                'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left);
+            }
+            //register callback
+            this.#callbackFunctions[msg_id] = resolve;
+        })
+    }
+
+    setWindowTitle = function(title, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('setWindowTitle', resolve, { new_title: title });
+        })
+    }
+
+    setWindowWidth = function(width, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('setWindowWidth', resolve, { width });
+        })
+    }
+
+    setWindowHeight = function(height, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('setWindowHeight', resolve, { height });
+        })
+    }
+
+    setWindowSize = function(width, height, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('setWindowSize', resolve, { width, height });
+        })
+    }
+
+    setWindowPosition = function(x, y, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('setWindowPosition', resolve, { x, y });
+        })
+    }
+
+    /**
+     * Asynchronously extracts entries from DataTransferItems, like files and directories.
+     * 
+     * @private
+     * @function
+     * @async
+     * @param {DataTransferItemList} dataTransferItems - List of data transfer items from a drag-and-drop operation.
+     * @param {Object} [options={}] - Optional settings.
+     * @param {boolean} [options.raw=false] - Determines if the file path should be processed.
+     * @returns {Promise<Array<File|Entry>>} - A promise that resolves to an array of File or Entry objects.
+     * @throws {Error} - Throws an error if there's an EncodingError and provides information about how to solve it.
+     * 
+     * @example
+     * const items = event.dataTransfer.items;
+     * const entries = await getEntriesFromDataTransferItems(items, { raw: false });
+     */
+    getEntriesFromDataTransferItems = async function(dataTransferItems, options = { raw: false }) {
+        const checkErr = (err) => {
+            if (this.getEntriesFromDataTransferItems.didShowInfo) return
+            if (err.name !== 'EncodingError') return
+            this.getEntriesFromDataTransferItems.didShowInfo = true
+            const infoMsg = `${err.name} occured within datatransfer-files-promise module\n`
+                + `Error message: "${err.message}"\n`
+                + 'Try serving html over http if currently you are running it from the filesystem.'
+            console.warn(infoMsg)
+        }
+
+        const readFile = (entry, path = '') => {
+            return new Promise((resolve, reject) => {
+                entry.file(file => {
+                    if (!options.raw) file.filepath = path + file.name // save full path
+                    resolve(file)
+                }, (err) => {
+                    checkErr(err)
+                    reject(err)
+                })
+            })
+        }
+
+        const dirReadEntries = (dirReader, path) => {
+            return new Promise((resolve, reject) => {
+                dirReader.readEntries(async entries => {
+                    let files = []
+                    for (let entry of entries) {
+                        const itemFiles = await getFilesFromEntry(entry, path)
+                        files = files.concat(itemFiles)
+                    }
+                    resolve(files)
+                }, (err) => {
+                    checkErr(err)
+                    reject(err)
+                })
+            })
+        }
+
+        const readDir = async (entry, path) => {
+            const dirReader = entry.createReader()
+            const newPath = path + entry.name + '/'
+            let files = []
+            let newFiles
+            do {
+                newFiles = await dirReadEntries(dirReader, newPath)
+                files = files.concat(newFiles)
+            } while (newFiles.length > 0)
+            return files
+        }
+
+        const getFilesFromEntry = async (entry, path = '') => {
+            if(entry === null)
+                return;
+            else if (entry.isFile) {
+                const file = await readFile(entry, path)
+                return [file]
+            }
+            else if (entry.isDirectory) {
+                const files = await readDir(entry, path)
+                files.push(entry)
+                return files
+            }
+        }
+
+        let files = []
+        let entries = []
+
+        // Pull out all entries before reading them
+        for (let i = 0, ii = dataTransferItems.length; i < ii; i++) {
+            entries.push(dataTransferItems[i].webkitGetAsEntry())
+        }
+
+        // Recursively read through all entries
+        for (let entry of entries) {
+            const newFiles = await getFilesFromEntry(entry)
+            files = files.concat(newFiles)
+        }
+
+        return files
+    }
+
+    authenticateWithPuter = function() {
+        if(this.env !== 'web'){
+            return;
+        }
+
+        // if authToken is already present, resolve immediately
+        if(this.authToken){
+            return new Promise((resolve) => {
+                resolve();
+            })
+        }
+
+        // If a prompt is already open, return a promise that resolves based on the existing prompt's result.
+        if (puter.puterAuthState.isPromptOpen) {
+            return new Promise((resolve, reject) => {
+                puter.puterAuthState.resolver = { resolve, reject };
+            });
+        }
+
+        // Show the permission prompt and set the state.
+        puter.puterAuthState.isPromptOpen = true;
+        puter.puterAuthState.authGranted = null;
+
+        return new Promise((resolve, reject) => {
+            if (!puter.authToken) {
+                const puterDialog = new PuterDialog(resolve, reject);
+                document.body.appendChild(puterDialog);
+                puterDialog.open();
+            } else {
+                // If authToken is already present, resolve immediately
+                resolve();
+            }
+        });
+    }
+
+    // Returns a Promise<AppConnection>
+    launchApp = function(appName, args, callback) {
+        return new Promise((resolve) => {
+            // if appName is an object and args is not set, then appName is actually args
+            if (typeof appName === 'object' && !args) {
+                args = appName;
+                appName = undefined;
+            }
+
+            this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args });
+        })
+    }
+
+    parentApp() {
+        return this.#parentAppConnection;
+    }
+
+    createWindow = function (options, callback) {
+        return new Promise((resolve) => {
+            this.#postMessageWithCallback('createWindow', resolve, { options: options ?? {} });
+        })
+    }
+
+    // Menubar
+    menubar = function(){
+        // Remove previous style tag
+        document.querySelectorAll('style.puter-stylesheet').forEach(function(el) {
+            el.remove();
+        })
+
+        // Add new style tag
+        const style = document.createElement('style');
+        style.classList.add('puter-stylesheet');
+        style.innerHTML = `
+        .--puter-menubar {
+            border-bottom: 1px solid #e9e9e9;
+            background-color: #fbf9f9;
+            padding-top: 3px;
+            padding-bottom: 2px;
+            display: inline-block;
+            position: fixed;
+            top: 0;
+            width: 100%;
+            margin: 0;
+            padding: 0;
+            height: 31px;
+            font-family: Arial, Helvetica, sans-serif;
+            font-size: 13px;
+            z-index: 9999;
+        }
+        
+        .--puter-menubar, .--puter-menubar * {
+            user-select: none;
+            -webkit-user-select: none;
+            cursor: default;
+        }
+        
+        .--puter-menubar .dropdown-item-divider>hr {
+            margin-top: 5px;
+            margin-bottom: 5px;
+            border-bottom: none;
+            border-top: 1px solid #00000033;
+        }
+        
+        .--puter-menubar>li {
+            display: inline-block;
+            padding: 10px 5px;
+        }
+        
+        .--puter-menubar>li>ul {
+            display: none;
+            z-index: 999999999999;
+            list-style: none;
+            background-color: rgb(233, 233, 233);
+            width: 200px;
+            border: 1px solid #e4ebf3de;
+            box-shadow: 0px 0px 5px #00000066;
+            padding-left: 6px;
+            padding-right: 6px;
+            padding-top: 4px;
+            padding-bottom: 4px;
+            color: #333;
+            border-radius: 4px;
+            padding: 2px;
+            min-width: 200px;
+            margin-top: 5px;
+            position: absolute;
+        }
+        
+        .--puter-menubar .menubar-item {
+            display: block;
+            line-height: 24px;
+            margin-top: -7px;
+            text-align: center;
+            border-radius: 3px;
+            padding: 0 5px;
+        }
+        
+        .--puter-menubar .menubar-item-open {
+            background-color: rgb(216, 216, 216);
+        }
+        
+        .--puter-menubar .dropdown-item {
+            padding: 5px;
+            padding: 5px 30px;
+            list-style-type: none;
+            user-select: none;
+            font-size: 13px;
+        }
+        
+        .--puter-menubar .dropdown-item-icon, .--puter-menubar .dropdown-item-icon-active {
+            pointer-events: none;
+            width: 18px;
+            height: 18px;
+            margin-left: -23px;
+            margin-bottom: -4px;
+            margin-right: 5px;
+        }
+        .--puter-menubar .dropdown-item-disabled .dropdown-item-icon{
+            display: inline-block !important;
+        }
+        .--puter-menubar .dropdown-item-disabled .dropdown-item-icon-active{
+            display: none !important;
+        }
+        .--puter-menubar .dropdown-item-icon-active {
+            display:none;
+        }
+        .--puter-menubar .dropdown-item:hover .dropdown-item-icon{
+            display: none;
+        }
+        .--puter-menubar .dropdown-item:hover .dropdown-item-icon-active{
+            display: inline-block;
+        }
+        .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon, .--puter-menubar .dropdown-item-hide-icon .dropdown-item-icon-active{
+            display: none !important;
+        }
+        .--puter-menubar .dropdown-item a {
+            color: #333;
+            text-decoration: none;
+        }
+        
+        .--puter-menubar .dropdown-item:hover, .--puter-menubar .dropdown-item:hover a {
+            background-color: rgb(59 134 226);
+            color: white;
+            border-radius: 4px;
+        }
+        
+        .--puter-menubar .dropdown-item-disabled, .--puter-menubar .dropdown-item-disabled:hover {
+            opacity: 0.5;
+            background-color: transparent;
+            color: initial;
+            cursor: initial;
+            pointer-events: none;
+        }
+        
+        .--puter-menubar .menubar * {
+            user-select: none;
+        }                
+        `;
+        let head = document.head || document.getElementsByTagName('head')[0];
+        head.appendChild(style);
+
+        document.addEventListener('click', function(e){
+            // Don't hide if clicking on disabled item
+            if(e.target.classList.contains('dropdown-item-disabled'))
+                return false;
+            // Hide open menus
+            if(!(e.target).classList.contains('menubar-item')){
+                document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
+                    el.classList.remove('menubar-item-open');
+                })
+
+                document.querySelectorAll('.dropdown').forEach(el => el.style.display = "none");
+            }
+        });
+
+        // When focus is gone from this window, hide open menus
+        window.addEventListener('blur', function(e){
+            document.querySelectorAll('.dropdown').forEach(function(el) {
+                el.style.display = "none";
+            })
+            document.querySelectorAll('.menubar-item.menubar-item-open').forEach(el => el.classList.remove('menubar-item-open'));
+        });
+
+        // Returns the siblings of the element
+        const siblings = function (e) {
+            const siblings = []; 
+
+            // if no parent, return empty list
+            if(!e.parentNode) {
+                return siblings;
+            }
+
+            // first child of the parent node
+            let sibling  = e.parentNode.firstChild;
+
+            // get all other siblings
+            while (sibling) {
+                if (sibling.nodeType === 1 && sibling !== e) {
+                    siblings.push(sibling);
+                }
+                sibling = sibling.nextSibling;
+            }
+            return siblings;
+        };
+
+        // Open dropdown
+        document.querySelectorAll('.menubar-item').forEach(el => el.addEventListener('mousedown', function(e){
+            // Hide all other menus
+            document.querySelectorAll('.dropdown').forEach(function(el) {
+                el.style.display = 'none';
+            });
+             
+            // Remove open class from all menus, except this menu that was just clicked
+            document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
+                if(el != e.target)
+                    el.classList.remove('menubar-item-open');
+            });
+            
+            // If menu is already open, close it
+            if(this.classList.contains('menubar-item-open')){
+                document.querySelectorAll('.menubar-item.menubar-item-open').forEach(function(el) {
+                    el.classList.remove('menubar-item-open');
+                });
+            }
+
+            // If menu is not open, open it
+            else if(!e.target.classList.contains('dropdown-item')){
+                this.classList.add('menubar-item-open')
+        
+                // show all sibling
+                siblings(this).forEach(function(el) {
+                    el.style.display = 'block';
+                });
+            }
+
+        }));
+
+        // If a menu is open, and you hover over another menu, open that menu
+        document.querySelectorAll('.--puter-menubar .menubar-item').forEach(el => el.addEventListener('mouseover', function(e){
+            const open_menus = document.querySelectorAll('.menubar-item.menubar-item-open');
+            if(open_menus.length > 0 && open_menus[0] !== e.target){
+                e.target.dispatchEvent(new Event('mousedown'));
+            }
+        }))
+    }
+
+    on(eventName, callback) {
+        super.on(eventName, callback);
+        // If we already received a broadcast for this event, run the callback immediately
+        if (this.#eventNames.includes(eventName) && this.#lastBroadcastValue.has(eventName)) {
+            callback(this.#lastBroadcastValue.get(eventName));
+        }
+    }
+}
+
+export default UI

+ 619 - 0
packages/puter-dot-js/test/fs.test.js

@@ -0,0 +1,619 @@
+naughtyStrings = [
+    "文件.txt",               // Chinese characters
+    "файл.txt",              // Cyrillic characters
+    "ファイル.txt",           // Japanese characters
+    "파일.txt",               // Korean characters
+    "ملف.txt",               // Arabic characters
+    "फ़ाइल.txt",             // Hindi characters
+    "archivo.txt",           // Spanish characters
+    "fichier.txt",           // French characters
+    "αρχείο.txt",            // Greek characters
+    "datei.txt",             // German characters
+    "fil.txt",               // Swedish characters
+    "קובץ.txt",              // Hebrew characters
+    "文件名.txt",             // Chinese characters
+    "файлы.txt",             // Russian characters
+    "फ़ाइलें.txt",           // Hindi characters
+    "📄_emoji.txt",           // Emoji
+    "file name with spaces.txt",
+    "file-name-with-dashes.txt",
+    "file_name_with_underscores.txt",
+    "file.name.with.periods.txt",
+    "file,name,with,commas.txt",
+    "file;name;with;semicolons.txt",
+    "file(name)with(parentheses).txt",
+    "file[name]with[brackets].txt",
+    "file{name}with{braces}.txt",
+    "file!name!with!exclamations!.txt",
+    "file@name@with@ats.txt",
+    "file#name#with#hashes#.txt",
+    "file$name$with$dollars$.txt",
+    "file%name%with%percentages%.txt",
+    "file^name^with^carats^.txt",
+    "file&name&with&amps&.txt",
+    "file*name*with*asterisks*.txt",
+    "file_name_with_long_name_exceeding_255_characters_abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz.txt",
+    "file👍name👍with👍thumbs👍up.txt",
+    "file😂name😂with😂emojis😂.txt",
+    "file🌍name🌍with🌍globe🌍emojis🌍.txt",
+    "file🔥name🔥with🔥fire🔥emoji🔥.txt",
+    "file🎉name🎉with🎉party🎉popper🎉emoji🎉.txt",
+    "file💼name💼with💼briefcase💼emoji💼.txt",
+    "file🍔name🍔with🍔burger🍔emoji🍔.txt",
+    "file🚀name🚀with🚀rocket🚀emoji🚀.txt",
+    "file👽name👽with👽alien👽emoji👽.txt",
+    "file🌈name🌈with🌈rainbow🌈emoji🌈.txt",
+    "file🍆name🍆with🍆eggplant🍆emoji🍆.txt",
+    "file🍑name🍑with🍑peach🍑emoji🍑.txt",
+    "invisible\u200Bname.txt",                  // Invisible Unicode character (Zero Width Space)
+    "invisible\u200Cname.txt",                  // Invisible Unicode character (Zero Width Non-Joiner)
+    "invisible\u200Dname.txt",                  // Invisible Unicode character (Zero Width Joiner)
+    "invisible\uFEFFname.txt",                  // Invisible Unicode character (Zero Width No-Break Space)
+    "invisible\u180Ename.txt",                  // Invisible Unicode character (Mongolian Vowel Separator)
+    "hash#tag.txt",
+    "percent%20encoded.txt",
+    "plus+sign.txt",
+    "ampersand&symbol.txt",
+    "at@symbol.txt",
+    "parentheses(1).txt",
+    "brackets[1].txt",
+    "curly{braces}.txt",
+    "angle<tags>.txt",
+    "exclamation!point.txt",
+    "question?mark.txt",
+    "colon:separated.txt",
+    "semicolon;separated.txt",
+    "single'quote.txt",
+    "double\"quote.txt",
+    "backtick`char.txt",
+    "tilde~sign.txt",
+    "underscore_character.txt",
+    "hyphen-character.txt",
+    "equal=sign.txt",
+    "plus+sign.txt",
+    "asterisk*char.txt",
+    "caret^char.txt",
+    "percent%sign.txt",
+    "dollar$sign.txt",
+    "pound#sign.txt",
+    "at@sign.txt",
+    "exclamation!mark.txt",
+    "question?mark.txt",
+    "backslash\\char.txt",
+    "pipe|char.txt",
+    "colon:char.txt",
+    "semicolon;char.txt",
+    "quote'char.txt",
+    "double\"quote.txt",
+    "backtick`char.txt",
+    "braces{char}.txt",
+    "brackets[char].txt",
+    "parentheses(char).txt",
+    "angle<brackets>.txt",
+    "ellipsis….txt",
+    "accentué.txt",
+    "ümlaut.txt",
+    "tildeñ.txt",
+    "çedilla.txt",
+    "špecial.txt",
+    "russianЯ.txt",
+    "chinese中文.txt",
+    "arabicعربى.txt",
+    "hebrewעברית.txt",
+    "japanese日本語.txt",
+    "korean한국어.txt",
+    "vietnameseTiếng Việt.txt",
+
+]
+
+window.fsTests = [
+    testFSWrite = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.write(randName, 'testValue');
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWrite passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                throw("testFSWrite failed to delete file:", error);
+            }
+        } catch (error) {
+            console.log(error);
+            throw("testFSWrite failed:", error);
+        }    
+    },
+    testFSRead = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await (await puter.fs.read(randName)).text();
+            assert(result === 'testValue', "Failed to read from file");
+            pass("testFSRead passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSRead failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSRead failed:", error);
+        }    
+    },
+    testFSWriteWithoutData = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.write(randName);
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteWithoutData passed");
+            if(randName !== result.name) {
+                fail(`testFSWriteWithoutData failed: Names do not match ${randName} ${result.name}`);
+            }
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSWriteWithoutData failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSWriteWithoutData failed:", error);
+        }    
+    },
+    testFSReadWithoutData = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName);
+            const result = await (await puter.fs.read(randName)).text();
+            assert(result === '', "Failed to read from file");
+            pass("testFSReadWithoutData passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSReadWithoutData failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSReadWithoutData failed:", error);
+        }    
+    },
+    testFSWriteToExistingFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.write(randName, 'updatedValue');
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteToExistingFile passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSWriteToExistingFile failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSWriteToExistingFile failed:", error);
+        }    
+    },
+    testFSWriteToExistingFileWithoutOverwriteAndDedupe = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.write(randName, 'updatedValue', { overwrite: false, dedupeName: false });
+            assert(!result.uid, "Failed to write to file");
+            fail("testFSWriteToExistingFileWithoutOverwriteAndDedupe failed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSWriteToExistingFileWithoutOverwriteAndDedupe failed to delete file:", error);
+            }
+        } catch (error) {
+            pass("testFSWriteToExistingFileWithoutOverwriteAndDedupe passed");
+        }
+    
+    },
+    testFSWriteToExistingFileWithoutOverwriteButWithDedupe = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.write(randName, 'updatedValue', { overwrite: false, dedupeName: true });
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteToExistingFileWithoutOverwriteButWithDedupe passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSWriteToExistingFileWithoutOverwriteButWithDedupe failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSWriteToExistingFileWithoutOverwriteButWithDedupe failed:", error);
+        }    
+    },
+    testFSWriteToExistingFileWithOverwriteButWithoutDedupe = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.write(randName, 'updatedValue', { overwrite: true, dedupeName: false });
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteToExistingFileWithOverwriteButWithoutDedupe passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSWriteToExistingFileWithOverwriteButWithoutDedupe failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSWriteToExistingFileWithOverwriteButWithoutDedupe failed:", error);
+        }    
+    },
+    // create a directory
+    testFSCreateDir = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.mkdir(randName);
+            assert(result.uid, "Failed to create directory");
+            pass("testFSCreateDir passed");
+        } catch (error) {
+            fail("testFSCreateDir failed:", error);
+        }    
+    },
+
+    // write a number of files to a directory and list them
+    testFSReadDir = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.mkdir(randName);
+            await puter.fs.write(randName + '/file1', 'testValue');
+            await puter.fs.write(randName + '/file2', 'testValue');
+            await puter.fs.write(randName + '/file3', 'testValue');
+            const result = await puter.fs.readdir(randName);
+            assert(result.length === 3, "Failed to read directory");
+            pass("testFSReadDir passed");
+        } catch (error) {
+            fail("testFSReadDir failed:", error);
+        }    
+    },
+
+    // create a file then delete it
+    testFSDelete = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.delete(randName);
+            assert(!result.uid, "Failed to delete file");
+            pass("testFSDelete passed");
+        } catch (error) {
+            fail("testFSDelete failed:", error);
+        }    
+    },
+
+    // create a directory, write a number of files to it, then delete it
+    testFSDeleteDir = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.mkdir(randName);
+            await puter.fs.write(randName + '/file1', 'testValue');
+            await puter.fs.write(randName + '/file2', 'testValue');
+            await puter.fs.write(randName + '/file3', 'testValue');
+            const result = await puter.fs.delete(randName);
+            assert(!result.uid, "Failed to delete directory");
+            pass("testFSDeleteDir passed");
+        } catch (error) {
+            fail("testFSDeleteDir failed:", error);
+        }    
+    },
+
+    // attempt to delete a non-existent file
+    testFSDeleteNonExistentFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.delete(randName);
+            assert(!result.uid, "Failed to delete non-existent file");
+            pass("testFSDeleteNonExistentFile passed");
+        } catch (error) {
+            if(error.code !== "subject_does_not_exist")
+                fail("testFSDeleteNonExistentFile failed:", error);
+            else
+                pass("testFSDeleteNonExistentFile passed");
+        }    
+    },
+
+    // attempt to access a non-existent file
+    testFSReadNonExistentFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.read(randName);
+            fail("testFSReadNonExistentFile failed");
+        } catch (error) {
+            if(error.code !== "subject_does_not_exist")
+                fail("testFSReadNonExistentFile failed:", error);
+            else
+                pass("testFSReadNonExistentFile passed");
+        }    
+    },
+
+    testFSWriteWithSpecialCharacters = async ()=>{
+        let randName
+        try {
+            randName = 'testFileWithSpecialCharacte rs!@#$%^&*()_+{}|:"<>?`~'
+            const result = await puter.fs.write(randName, 'testValue', { specialCharacters: true });
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteWithSpecialCharacters passed");
+        } catch (error) {
+            fail("testFSWriteWithSpecialCharacters failed:", error);
+        }    
+
+        // delete the file
+        try {
+            await puter.fs.delete(randName);
+        } catch (error) {
+            fail("testFSWriteWithSpecialCharacters failed to delete file:", error);
+        }
+    },
+
+    testFSReadWithSpecialCharacters = async ()=>{
+        try {
+            let randName = 'testFileWithSpecialCharacte rs!@#$%^&*()_+{}|:"<>?`~'
+            await puter.fs.write(randName, 'testValue');
+            const result = await (await puter.fs.read(randName)).text();
+            assert(result === 'testValue', "Failed to read from file");
+            pass("testFSReadWithSpecialCharacters passed");
+        } catch (error) {
+            fail("testFSReadWithSpecialCharacters failed:", error);
+        }    
+    },
+
+    testFSWriteLargeFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            const result = await puter.fs.write(randName, 'testValue'.repeat(100000));
+            assert(result.uid, "Failed to write to file");
+            pass("testFSWriteLargeFile passed");
+        } catch (error) {
+            fail("testFSWriteLargeFile failed:", error);
+        }    
+    },
+
+    testFSReadLargeFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue'.repeat(100000));
+            const result = await (await puter.fs.read(randName)).text();
+            assert(result === 'testValue'.repeat(100000), "Failed to read from file");
+            pass("testFSReadLargeFile passed");
+        } catch (error) {
+            fail("testFSReadLargeFile failed:", error);
+        }    
+    },
+
+    testFSRenameFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            let randName2 = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            const result = await puter.fs.rename(randName, randName2);
+            assert(result.name, "Failed to rename file");
+            pass("testFSRenameFile passed");
+            // check that the old file is gone
+            try {
+                await puter.fs.read(randName);
+                fail("testFSRenameFile failed to delete old file");
+            } catch (error) {
+                if(error.code !== "subject_does_not_exist")
+                    fail("testFSRenameFile failed to delete old file:", error);
+                else
+                    pass("testFSRenameFile passed");
+            }
+        } catch (error) {
+            fail("testFSRenameFile failed:", error);
+        }    
+    },
+
+    testFSMoveFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            let randName2 = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            await puter.fs.mkdir(randName2);
+            let result = await puter.fs.move(randName, randName2);
+            assert(result.moved, "Failed to move file");
+            // check that the old file is gone
+            try {
+                await puter.fs.read(randName);
+                fail("testFSMoveFile failed to delete old file");
+            } catch (error) {
+                if(error.code !== "subject_does_not_exist")
+                    fail("testFSMoveFile failed to delete old file:", error);
+                else
+                    pass("testFSMoveFile passed");
+            }
+        } catch (error) {
+            fail("testFSMoveFile failed:", error);
+        }    
+    },
+    
+    testFSCopyFile = async ()=>{
+        try {
+            let randName = puter.randName();
+            let randName2 = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            await puter.fs.mkdir(randName2);
+            let result = await puter.fs.copy(randName, randName2);
+            assert(Array.isArray(result) && result[0].uid, "Failed to copy file");
+            // check that the old file is still there
+            try {
+                await puter.fs.read(randName);
+                pass("testFSCopyFile passed");
+            } catch (error) {
+                fail("testFSCopyFile failed to keep old file:", error);
+            }
+        } catch (error) {
+            fail("testFSCopyFile failed:", error);
+        }    
+    },
+
+    // copy a file to a directory with newName option
+    testFSCopyFileWithNewName = async ()=>{
+        try {
+            let randName = puter.randName();
+            let randName2 = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            await puter.fs.mkdir(randName2);
+            let result = await puter.fs.copy(randName, randName2, { newName: 'newName' });
+            assert(Array.isArray(result) && result[0].uid, "Failed to copy file");
+            // check file name
+            assert(result[0].name === 'newName', "Failed to copy file with new name");
+            // check that the old file is still there
+            try {
+                await puter.fs.read(randName);
+                pass("testFSCopyFileWithNewName passed");
+            } catch (error) {
+                fail("testFSCopyFileWithNewName failed to keep old file:", error);
+            }
+        } catch (error) {
+            fail("testFSCopyFileWithNewName failed:", error);
+        }    
+    },
+
+    testFSStat = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.write(randName, 'testValue');
+            let result = await puter.fs.stat(randName);
+            assert(result.uid, "Failed to stat file");
+            pass("testFSStat passed");
+        } catch (error) {
+            fail("testFSStat failed:", error);
+        }    
+    },
+
+    testFSStatDir = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.mkdir(randName);
+            let result = await puter.fs.stat(randName);
+            assert(result.uid, "Failed to stat directory");
+            pass("testFSStatDir passed");
+        } catch (error) {
+            fail("testFSStatDir failed:", error);
+        }    
+    },
+
+    testFSStatNonExistent = async ()=>{
+        try {
+            let randName = puter.randName();
+            let result = await puter.fs.stat(randName);
+            fail("testFSStatNonExistent failed");
+        } catch (error) {
+            if(error.code !== "subject_does_not_exist")
+                fail("testFSStatNonExistent failed:", error);
+            else
+                pass("testFSStatNonExistent passed");
+        }    
+    },
+
+    // create a directory, write a number of files to it, then delete it
+    testFSDeleteDirWithFiles = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.mkdir(randName);
+            await puter.fs.write(randName + '/file1', 'testValue');
+            await puter.fs.write(randName + '/file2', 'testValue');
+            await puter.fs.write(randName + '/file3', 'testValue');
+            const result = await puter.fs.delete(randName, { recursive: true });
+            assert(!result.uid, "Failed to delete directory");
+            pass("testFSDeleteDirWithFiles passed");
+        } catch (error) {
+            fail("testFSDeleteDirWithFiles failed:", error);
+        }    
+    },
+    // check if stat on a directory returns name, path, is_dir
+    testFSStatDirReturnsAttrs = async ()=>{
+        try {
+            let randName = puter.randName();
+            await puter.fs.mkdir(randName);
+            let result = await puter.fs.stat(randName);
+            assert(result.name && typeof result.name === 'string', "Failed to stat directory (name)");
+            assert(result.path && typeof result.path === 'string', "Failed to stat directory (path)");
+            assert(result.immutable !== undefined, "Failed to stat directory (immutable)");
+            assert(result.metadata !== undefined, "Failed to stat directory (metadata)");
+            assert(result.modified !== undefined, "Failed to stat directory (modified)");
+            assert(result.created !== undefined, "Failed to stat directory (created)");
+            assert(result.accessed !== undefined, "Failed to stat directory (accessed)");
+            assert(result.size !== undefined, "Failed to stat directory (size)");
+            assert(result.layout !== undefined, "Failed to stat directory (layout)");
+            assert(result.owner !== undefined && typeof result.owner === 'object', "Failed to stat directory (owner)");
+            assert(result.dirname !== undefined && typeof result.dirname === 'string', "Failed to stat directory (dirname)");
+            assert(result.parent_id !== undefined && typeof result.parent_id === 'string', "Failed to stat directory (parent_id)");
+            // todo this will fail for now until is_dir is turned into boolean
+            assert(result.is_dir !== undefined && typeof result.is_dir === 'boolean' && result.is_dir === true, "Failed to stat directory (is_dir)");
+            assert(result.is_empty !== undefined && typeof result.is_empty === 'boolean', "Failed to stat directory (is_empty)");
+            pass("testFSStatDirReturnsAttrs passed");
+        } catch (error) {
+            throw("testFSStatDirReturnsAttrs failed:", error);
+        }    
+    },
+
+    // test read() with the object returned by write()
+    testFSReadWithWriteResult = async ()=>{
+        try {
+            let randName = puter.randName();
+            let writeResult = await puter.fs.write(randName, 'testValue');
+            let result = await (await puter.fs.read(writeResult)).text();
+            assert(result === 'testValue', "Failed to read from file");
+            pass("testFSReadWithWriteResult passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSReadWithWriteResult failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSReadWithWriteResult failed:", error);
+        }    
+    },
+
+    // test stat() with the object returned by write()
+    testFSStatWithWriteResult = async ()=>{
+        try {
+            let randName = puter.randName();
+            let writeResult = await puter.fs.write(randName, 'testValue');
+            let result = await puter.fs.stat(writeResult);
+            assert(result.uid, "Failed to stat file");
+            pass("testFSStatWithWriteResult passed");
+            // delete the file
+            try {
+                await puter.fs.delete(randName);
+            } catch (error) {
+                fail("testFSStatWithWriteResult failed to delete file:", error);
+            }
+        } catch (error) {
+            fail("testFSStatWithWriteResult failed:", error);
+        }    
+    },
+
+    // test creating files with names from naughtyStrings
+    testFSWriteWithNaughtyStrings = async ()=>{
+        try {
+            let randName = puter.randName();
+            for(let i = 0; i < naughtyStrings.length; i++) {
+                let filename = randName + naughtyStrings[i];
+                console.log(filename);
+                let result = await puter.fs.write(filename, 'testValue');
+                assert(result.uid, "Failed to write to file");
+                // check name
+                assert(result.name === filename, "Failed to write to file with naughty name: " + filename);
+                // delete the file
+                try {
+                    await puter.fs.delete(filename);
+                } catch (error) {
+                    fail("testFSWriteWithNaughtyStrings failed to delete file: " + filename, error);
+                }
+            }
+            pass("testFSWriteWithNaughtyStrings passed");
+        } catch (error) {
+            console.log(error);
+            fail("testFSWriteWithNaughtyStrings failed:", error);
+        }    
+    },
+];

+ 417 - 0
packages/puter-dot-js/test/kv.test.js

@@ -0,0 +1,417 @@
+window.kvTests = [
+    testSetKeyWithValue = async function() {
+        try {
+            const result = await puter.kv.set('testKey', 'testValue');
+            assert(result === true, "Failed to set key with value");
+            pass("testSetKeyWithValue passed");
+        } catch (error) {
+            fail("testSetKeyWithValue failed:", error);
+        }
+    },
+    
+    testUpdateKey = async function() {
+        try {
+            await puter.kv.set('updateKey', 'initialValue');
+            const result = await puter.kv.set('updateKey', 'updatedValue');
+            assert(result === true, "Failed to update existing key");
+            pass("testUpdateKey passed");
+        } catch (error) {
+            fail("testUpdateKey failed:", error);
+        }
+    },
+    
+    testKeySizeLimit = async function() {
+        try {
+            const largeKey = 'a'.repeat(1025); // 1 KB + 1 byte
+            await puter.kv.set(largeKey, 'value');
+            fail("testKeySizeLimit failed: No error thrown for large key");
+        } catch (error) {
+            pass("testKeySizeLimit passed:", error.message);
+        }
+    },
+    
+    testInvalidParameters = async function() {
+        try {
+            await puter.kv.set(undefined, 'value');
+            fail("testInvalidParameters failed: No error thrown for undefined key");
+        } catch (error) {
+            pass("testInvalidParameters passed:", error.message);
+        }
+    },
+    
+    // testEmptyKey should fail
+    testEmptyKey = async function() {
+        try {
+            await puter.kv.set('', 'value');
+            fail("testEmptyKey failed: No error thrown for empty key");
+        } catch (error) {
+            pass("testEmptyKey passed:", error.message);
+        }
+    },
+    
+    
+    testSetNullValue = async function() {
+        try {
+            const result = await puter.kv.set('nullValueKey', null);
+            assert(result === true, "Failed to set null value");
+            pass("testSetNullValue passed");
+        } catch (error) {
+            fail("testSetNullValue failed:", error);
+        }
+    },
+    
+    testSetObjectValue = async function() {
+        try {
+            const result = await puter.kv.set('objectKey', { a: 1 });
+            assert(result === true, "Failed to set object as value");
+            pass("testSetObjectValue passed");
+        } catch (error) {
+            fail("testSetObjectValue failed:", error);
+        }
+    },
+    
+    testSetKeyWithSpecialCharacters = async function() {
+        try {
+            const result = await puter.kv.set('special@Key#', 'value');
+            assert(result === true, "Failed to set key with special characters");
+            pass("testSetKeyWithSpecialCharacters passed");
+        } catch (error) {
+            fail("testSetKeyWithSpecialCharacters failed:", error);
+        }
+    },
+    
+    testSetLargeValue = async function() {
+        try {
+            const largeValue = 'a'.repeat(10000); // 10 KB
+            const result = await puter.kv.set('largeValueKey', largeValue);
+            assert(result === true, "Failed to set large value");
+            pass("testSetLargeValue passed");
+        } catch (error) {
+            fail("testSetLargeValue failed:", error);
+        }
+    },
+    
+    testSetBooleanValue = async function() {
+        try {
+            const result = await puter.kv.set('booleanKey', true);
+            assert(result === true, "Failed to set boolean value");
+            pass("testSetBooleanValue passed");
+        } catch (error) {
+            fail("testSetBooleanValue failed:", error);
+        }
+    },
+    
+    testSetNumericKey = async function() {
+        try {
+            const result = await puter.kv.set(123, 'value');
+            assert(result === true, "Failed to set numeric key");
+            pass("testSetNumericKey passed");
+        } catch (error) {
+            fail("testSetNumericKey failed:", error);
+        }
+    },
+    
+    testSetConcurrentKeys = async function() {
+        try {
+            const promises = [puter.kv.set('key1', 'value1'), puter.kv.set('key2', 'value2')];
+            const results = await Promise.all(promises);
+            assert(results.every(result => result === true), "Failed to set concurrent keys");
+            pass("testSetConcurrentKeys passed");
+        } catch (error) {
+            fail("testSetConcurrentKeys failed:", error);
+        }
+    },
+    
+    testSetValueAndRetrieve = async function() {
+        try {
+            await puter.kv.set('retrieveKey', 'testValue');
+            const value = await puter.kv.get('retrieveKey');
+            assert(value === 'testValue', "Failed to retrieve correct value");
+            pass("testSetValueAndRetrieve passed");
+        } catch (error) {
+            fail("testSetValueAndRetrieve failed:", error);
+        }
+    },
+    
+    testUpdateValueAndRetrieve = async function() {
+        try {
+            await puter.kv.set('updateKey', 'initialValue');
+            await puter.kv.set('updateKey', 'updatedValue');
+            const value = await puter.kv.get('updateKey');
+            assert(value === 'updatedValue', "Failed to retrieve updated value");
+            pass("testUpdateValueAndRetrieve passed");
+        } catch (error) {
+            fail("testUpdateValueAndRetrieve failed:", error);
+        }
+    },
+    
+    testSetNumericValueAndRetrieve = async function() {
+        try {
+            await puter.kv.set('numericKey', 123);
+            const value = await puter.kv.get('numericKey');
+            assert(value === 123, "Failed to retrieve numeric value");
+            pass("testSetNumericValueAndRetrieve passed");
+        } catch (error) {
+            fail("testSetNumericValueAndRetrieve failed:", error);
+        }
+    },
+    
+    testSetBooleanValueAndRetrieve = async function() {
+        try {
+            await puter.kv.set('booleanKey', true);
+            const value = await puter.kv.get('booleanKey');
+            assert(value === true, "Failed to retrieve boolean value");
+            pass("testSetBooleanValueAndRetrieve passed");
+        } catch (error) {
+            fail("testSetBooleanValueAndRetrieve failed:", error);
+        }
+    },
+    
+    
+    testSetAndDeleteKey = async function() {
+        try {
+            await puter.kv.set('deleteKey', 'value');
+            const result = await puter.kv.del('deleteKey');
+            assert(result === true, "Failed to delete key");
+            pass("testSetAndDeleteKey passed");
+        } catch (error) {
+            fail("testSetAndDeleteKey failed:", error);
+        }
+    },
+
+    // if key does not exist, get() should return null
+    testGetNonexistentKey = async function() {
+        try {
+            const value = await puter.kv.get('nonexistentKey_102mk');
+            assert(value === null, "Failed to return `null` for nonexistent key");
+            pass("testGetNonexistentKey passed");
+        } catch (error) {
+            fail("testGetNonexistentKey failed:", error);
+        }
+    },
+    
+    // string key and object value
+    testSetObjectValue = async function() {
+        try {
+            const result = await puter.kv.set('objectKey', { a: 1 });
+            assert(result === true, "Failed to set object as value");
+            const value = await puter.kv.get('objectKey');
+            assert(value.a === 1, "Failed to retrieve object value");
+            pass("testSetObjectValue passed");
+        } catch (error) {
+            fail("testSetObjectValue failed:", error);
+        }
+    },
+
+    // string key and array value
+    testSetArrayValue = async function() {
+        try {
+            const result = await puter.kv.set('arrayKey', [1, 2, 3]);
+            assert(result === true, "Failed to set array as value");
+            const value = await puter.kv.get('arrayKey');
+            assert(value[0] === 1, "Failed to retrieve array value");
+            pass("testSetArrayValue passed");
+        } catch (error) {
+            fail("testSetArrayValue failed:", error);
+        }
+    },
+
+    testSetKeyWithSpecialCharactersAndRetrieve = async function() {
+        try {
+            await puter.kv.set('special@Key#', 'value');
+            const value = await puter.kv.get('special@Key#');
+            assert(value === 'value', "Failed to retrieve value for key with special characters");
+            pass("testSetKeyWithSpecialCharactersAndRetrieve passed");
+        } catch (error) {
+            fail("testSetKeyWithSpecialCharactersAndRetrieve failed:", error);
+        }
+    },
+    
+    testConcurrentSetOperations = async function() {
+        try {
+            const promises = [puter.kv.set('key1', 'value1'), puter.kv.set('key2', 'value2')];
+            const results = await Promise.all(promises);
+            assert(results.every(result => result === true), "Failed to set concurrent keys");
+            pass("testConcurrentSetOperations passed");
+        } catch (error) {
+            fail("testConcurrentSetOperations failed:", error);
+        }
+    },
+
+    //test flush: create a bunch of keys, flush, then check if they exist
+    testFlush = async function() {
+        try {
+            const keys = [];
+            for(let i = 0; i < 10; i++){
+                keys.push('key' + i);
+            }
+            await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
+            await puter.kv.flush();
+            const results = await Promise.all(keys.map(key => puter.kv.get(key)));
+            assert(results.every(result => result === null), "Failed to flush keys");
+            pass("testFlush passed");
+        } catch (error) {
+            fail("testFlush failed:", error);
+        }
+    },
+
+    // incr
+    testIncr = async function() {
+        try {
+            const result = await puter.kv.incr('incrKey');
+            assert(result === 1, "Failed to increment key");
+            pass("testIncr passed");
+        } catch (error) {
+            fail("testIncr failed:", error);
+        }
+    },
+
+    // decr
+    testDecr = async function() {
+        try {
+            const result = await puter.kv.decr('decrKey');
+            assert(result === -1, "Failed to decrement key");
+            pass("testDecr passed");
+        } catch (error) {
+            fail("testDecr failed:", error);
+        }
+    },
+
+    // incr existing key
+    testIncrExistingKey = async function() {
+        try {
+            await puter.kv.set('incrKey', 1);
+            const result = await puter.kv.incr('incrKey');
+            assert(result === 2, "Failed to increment existing key");
+            pass("testIncrExistingKey passed");
+        } catch (error) {
+            fail("testIncrExistingKey failed:", error);
+        }
+    },
+
+    // decr existing key
+    testIncrExistingKey = async function() {
+        try {
+            await puter.kv.set('decrKey', 2);
+            const result = await puter.kv.decr('decrKey');
+            assert(result === 1, "Failed to decrement existing key");
+            pass("testDecrExistingKey passed");
+        } catch (error) {
+            fail("testDecrExistingKey failed:", error);
+        }
+    },
+
+    // incr by amount
+    testIncrByAmount = async function() {
+        try {
+            await puter.kv.set('incrKey', 1);
+            const result = await puter.kv.incr('incrKey', 5);
+            assert(result === 6, "Failed to increment key by amount");
+            pass("testIncrByAmount passed");
+        } catch (error) {
+            fail("testIncrByAmount failed:", error);
+        }
+    },
+
+    // decr by amount
+    testDecrByAmount = async function() {
+        try {
+            await puter.kv.set('decrKey', 10);
+            const result = await puter.kv.decr('decrKey', 5);
+            assert(result === 5, "Failed to decrement key by amount");
+            pass("testDecrByAmount passed");
+        } catch (error) {
+            fail("testDecrByAmount failed:", error);
+        }
+    },
+
+    // incr by amount existing key
+    testIncrByAmountExistingKey = async function() {
+        try {
+            await puter.kv.set('incrKey', 1);
+            const result = await puter.kv.incr('incrKey', 5);
+            assert(result === 6, "Failed to increment existing key by amount");
+            pass("testIncrByAmountExistingKey passed");
+        } catch (error) {
+            fail("testIncrByAmountExistingKey failed:", error);
+        }
+    },
+
+    // decr by amount existing key
+    testDecrByAmountExistingKey= async function() {
+        try {
+            await puter.kv.set('decrKey', 10);
+            const result = await puter.kv.decr('decrKey', 5);
+            assert(result === 5, "Failed to decrement existing key by amount");
+            pass("testDecrByAmountExistingKey passed");
+        } catch (error) {
+            fail("testDecrByAmountExistingKey failed:", error);
+        }
+    },
+
+    // incr by negative amount
+    testIncrByNegativeAmount = async function() {
+        try {
+            await puter.kv.set('incrKey', 1);
+            const result = await puter.kv.incr('incrKey', -5);
+            assert(result === -4, "Failed to increment key by negative amount");
+            pass("testIncrByNegativeAmount passed");
+        } catch (error) {
+            fail("testIncrByNegativeAmount failed:", error);
+        }
+    },
+
+    // decr by negative amount
+    testDecrByNegativeAmount = async function() {
+        try {
+            await puter.kv.set('decrKey', 10);
+            const result = await puter.kv.decr('decrKey', -5);
+            assert(result === 15, "Failed to decrement key by negative amount");
+            pass("testDecrByNegativeAmount passed");
+        } catch (error) {
+            fail("testDecrByNegativeAmount failed:", error);
+        }
+    },
+
+    // list keys
+    testListKeys = async function() {
+        try {
+            const keys = [];
+            // flush first
+            await puter.kv.flush();
+            // create 10 keys
+            for(let i = 0; i < 10; i++){
+                keys.push('key' + i);
+            }
+            // set all keys
+            await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
+            // list keys
+            const result = await puter.kv.list();
+            assert(result.length === 10, "Failed to list keys");
+            pass("testListKeys passed");
+        } catch (error) {
+            fail("testListKeys failed:", error);
+        }
+    },
+
+    // list keys using glob
+    testListKeysGlob = async function() {
+        try {
+            const keys = [];
+            // flush first
+            await puter.kv.flush();
+            // create 10 keys
+            for(let i = 0; i < 10; i++){
+                keys.push('key' + i);
+            }
+            // set all keys
+            await Promise.all(keys.map(key => puter.kv.set(key, 'value')));
+            // list keys
+            const result = await puter.kv.list('k*');
+            assert(result.length === 10, "Failed to list keys using glob");
+            pass("testListKeysGlob passed");
+        } catch (error) {
+            fail("testListKeysGlob failed:", error);
+        }
+    },
+]

+ 137 - 0
packages/puter-dot-js/test/run.html

@@ -0,0 +1,137 @@
+<html>
+<head>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="../dist/puter.dev.js"></script>
+    <script src="./kv.test.js"></script>
+    <script src="./fs.test.js"></script>
+    <style>
+        #tests {
+            margin-top: 20px;
+        }
+        #run-tests {
+            margin-top: 20px;
+            margin-bottom: 20px;
+            background-color: #4c84af;
+            border: none;
+            color: white;
+            padding: 10px 20px;
+            text-align: center;
+            text-decoration: none;
+            display: inline-block;
+            font-size: 16px;
+            cursor: pointer;
+        }
+        #unselect-all {
+            margin-left: 20px;
+            cursor: pointer;
+        }
+        #select-all {
+            margin-left: 20px;
+            cursor: pointer;
+        }
+        .test-container{
+            margin-bottom: 10px;
+            padding: 10px;
+            border-radius: 5px;
+        }
+    </style>
+    <script>
+    document.addEventListener("DOMContentLoaded", () => {
+
+        window.pass = function(msg) {
+            // $('#tests').append(`<p style="color:green;">${msg}</p>`);
+        }
+
+        window.fail = function(msg) {
+            throw new Error(msg);
+        }
+        
+        // print the test name with checkbox for each test
+        $('#tests').append('<h2>File System Tests</h2>');
+        for (let i = 0; i < fsTests.length; i++) {
+            $('#tests').append(`<div class="test-container" id="fsTests-container-${i}">
+                <input type="checkbox" class="test-checkbox" id="fsTests${i}" checked>
+                <label for="fsTests${i}">${fsTests[i].name}</label><br>
+            </div>`);
+        }
+
+        $('#tests').append('<h2>Key Value Tests</h2>');
+        for (let i = 0; i < kvTests.length; i++) {
+            $('#tests').append(`<div class="test-container" id="kvTests-container-${i}">
+                <input type="checkbox" class="test-checkbox" id="kvTests${i}" checked>
+                <label for="kvTests${i}">${kvTests[i].name}</label><br>
+            </div>`);
+        }
+
+        window.assert = function(condition, message) {
+            if (!condition) {
+                throw new Error(message || "Assertion failed");
+            }
+        }
+
+        async function runTests() {
+            // go through fsTests and run each test
+            for (let i = 0; i < fsTests.length; i++) {
+                if (document.getElementById(`fsTests${i}`).checked) {
+                    try{
+                        await fsTests[i]();
+                        // make this test's container green
+                        $(`#fsTests-container-${i}`).css('background-color', '#85e085');
+
+                    } catch (e) {
+                        console.log(e);
+                        // make this test's container red
+                        $(`#fsTests-container-${i}`).css('background-color', '#ffbfbf');
+                        // message
+                        $(`#fsTests-container-${i}`).append(`<p style="color:#c00000;">${e}</p>`);
+                    }
+                }
+            }
+
+            for (let i = 0; i < kvTests.length; i++) {
+                if (document.getElementById(`kvTests${i}`).checked) {
+                    try{
+                        await kvTests[i]();
+                        // make this test's container green
+                        $(`#kvTests-container-${i}`).css('background-color', '#85e085');
+
+                    } catch (e) {
+                        // make this test's container red
+                        $(`#kvTests-container-${i}`).css('background-color', '#ff8484');
+                        // message
+                        $(`#kvTests-container-${i}`).append(`<p style="color:red;">${e}</p>`);
+                    }
+                }
+            }
+        }
+
+        $('#run-tests').click(() => {
+            runTests();
+        });
+
+        $('#unselect-all').click(() => {
+            for (let i = 0; i < fsTests.length; i++) {
+                $('.test-checkbox').prop('checked', false);
+            }
+        });
+
+        $('#select-all').click(() => {
+            for (let i = 0; i < fsTests.length; i++) {
+                $('.test-checkbox').prop('checked', true);
+            }
+        });
+
+    });
+
+    </script>
+</head>
+<body>
+
+    <nav style="position: fixed; top: 0; width: 100%; background: #EEE; left: 0; padding-left: 10px;">
+        <button id="run-tests">Run Tests</button>
+        <span id="select-all">Select All</span>
+        <span id="unselect-all">Unselect All</span>
+    </nav>
+    <div id="tests" style="margin-top:100px;"></div>
+</body>
+</html>

+ 2 - 3
src/index.js

@@ -53,11 +53,9 @@ window.gui = async function(options){
     window.max_item_name_length = options.max_item_name_length ?? 500;
     window.require_email_verification_to_publish_website = options.require_email_verification_to_publish_website ?? true;
 
-    // Add Puter.JS
-    await loadScript('https://js.puter.com/v2/');
-
     // DEV: Load the initgui.js file if we are in development mode
     if(!window.gui_env || window.gui_env === "dev"){
+        await loadScript('/sdk/puter.dev.js');
         await loadScript('/initgui.js', {isModule: true});
     }
     
@@ -65,6 +63,7 @@ window.gui = async function(options){
     // note: the order of the bundles is important
     // note: Build script will prepend `window.gui_env="prod"` to the top of the file
     else if(gui_env === "prod"){
+        await loadScript('https://js.puter.com/v2/');
         // Load the minified bundles
         await loadCSS('/dist/bundle.min.css');
         await loadScript('/dist/bundle.min.js');

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác